Gregorics Tibor PROGRAMOZÁS. 2. kötet MEGVALÓSÍTÁS

Méret: px
Mutatás kezdődik a ... oldaltól:

Download "Gregorics Tibor PROGRAMOZÁS. 2. kötet MEGVALÓSÍTÁS"

Átírás

1 Gregorics Tibor PROGRAMOZÁS 2. kötet MEGVALÓSÍTÁS 1

2 Egyetemi jegyzet

3 ELŐSZÓ... 8 BEVEZETÉS I. RÉSZ ALAPOK ELSŐ LÉPÉSEK Implementációs stratégia Nyelvi elemek Feladat: Osztási maradék Feladat: Változó csere C++ kislexikon STRUKTURÁLT PROGRAMOK Implementációs stratégia Nyelvi elemek Feladat: Másodfokú egyenlet Feladat: Legnagyobb közös osztó Feladat: Legnagyobb közös osztó még egyszer C++ kislexikon TÖMBÖK Implementációs stratégia Nyelvi elemek Feladat: Tömb maximális eleme Feladat: Mátrix maximális eleme Feladat: Melyik szóra gondoltam C++ kislexikon KONZOLOS BE- ÉS KIMENET Implementációs stratégia Nyelvi elemek

4 9. Feladat: Duna vízállása Feladat: Alsóháromszög-mátrix C++ kislexikon SZÖVEGES ÁLLOMÁNYOK Implementációs stratégia Nyelvi elemek Feladat: Szöveges állomány maximális eleme Feladat: Jó tanulók kiválogatása C++ kislexikon II. RÉSZ PROCEDURÁLIS PROGRAMOZÁS ALPROGRAMOK A KÓDBAN Implementációs stratégia Nyelvi elemek Feladat: Faktoriális Feladat: Adott számmal osztható számok Feladat: Páros számok darabszáma C++ kislexikon PROGRAMOZÁSI TÉTELEK IMPLEMENTÁLÁSA Implementációs stratégia Nyelvi elemek Feladat: Legnagyobb osztó Feladat: Legkisebb adott tulajdonságú elem Feladat: Keressünk Ibolyát C++ kislexikon TÖBBSZÖRÖS VISSZAVEZETÉS ALPROGRAMOKKAL Implementációs stratégia

5 Nyelvi elemek Feladat: Kitűnő tanuló Feladat: Azonos színű oldalak Feladat: Mátrix párhozamos átlói C++ kislexikon FORDÍTÁSI EGYSÉGEKRE BONTOTT PROGRAM Implementációs stratégia Nyelvi elemek Feladat: Műkorcsolya verseny Feladat: Melyikből hány van C++ kislexikon REKURZÍV PROGRAMOK KÓDOLÁSA Implementációs stratégia Nyelvi elemek Feladat: Binomiális együttható Feladat: Hanoi tornyai Feladat: Quick sort III. RÉSZ PROGRAMOZÁS OSZTÁLYOKKAL A TÍPUS MEGVALÓSÍTÁS ESZKÖZE: AZ OSZTÁLY Implementációs stratégia Nyelvi háttér Feladat: UFO-k Feladat: Zsák Feladat: Síkvektorok C++ kislexikon FELSOROLÓK TÍPUSAINAK MEGVALÓSÍTÁSA

6 Implementációs stratégia Nyelvi háttér Feladat: Könyvtár Feladat: Havi átlag-hőmérséklet Feladat: Bekezdések C++ kislexikon DINAMIKUS SZERKEZETŰ TÍPUSOK OSZTÁLYAI Implementációs stratégia Nyelvi elemek Feladat: Verem Feladat: Kettős sor C++ kislexikon OBJEKTUM-ORIENTÁLT KÓD-ÚJRAFELHASZNÁLÁSI TECHNIKÁK Implementációs stratégia Nyelvi elemek Feladat: Túlélési verseny Feladat: Lengyel forma és kiértékelése Feladat: Bináris fa bejárása C++ kislexikon EGY OSZTÁLY-SABLON KÖNYVTÁR FELHASZNÁLÁSA Osztály-sablon könyvtár tervezése Osztály-sablon könyvtár implementálása Feladat: Kiválogatás Feladat: Feltételes maximumkeresés Feladat: Keresés Feladat: Leghosszabb szó W betűvel

7 42. Feladat: Összefuttatás IRODALOM JEGYZÉK

8 ELŐSZÓ Ez a könyv annak a Programozás című sorozatnak a második kötete, amelyet az Eötvös Loránd Tudományegyetem programtervező informatikus szakának azon tantárgyaihoz ajánlok, amelyeken a hallgatók az első benyomásaikat szerezhetik meg a programozás szakmájáról. Az első kötet a Tervezés alcímet viseli, és a programozási feladatokat megoldó algoritmusok előállításával foglalkozik. Ennek a kötetnek pedig a Megvalósítás az alcíme, mert itt az előző kötet alapján elkészített tervek kivitelezéséről lesz szó, amely során a tervet egy konkrét programozási környezetben implementáljuk, programkódot hozunk létre és futtatható alkalmazást készítünk. Ebben a kötetben is a tervezéssel kezdem egy feladat megoldását, de azt nem kommentálom, hiszen erről az első kötetben részletesen esik szó. Inkább azokra a kérdésekre szeretnék válaszolni, amelyek az implementáció során vetődnek fel. Olyan döntési szituációkat, stratégiákat veszek sorra, amelyekkel a programozó a terv megvalósítása során találkozik. Mivel pedig a végcél egy szoftver előállítása, ezért kellő figyelmet fordítok majd a tesztelésre, sőt, a megoldások leírásával mintát adok a kielégítő dokumentálásra is. Egy olyan könyvben, amelyik működő alkalmazások készítéséről szól, megkerülhetetlen annak a programozási nyelvnek a bemutatása, amellyel programjainkat kódoljuk. Nincs szándékom azonban programozási nyelvkönyvet írni, hiszen ezekből sok jót találni a könyvesboltokban. Az implementáció folyamata, döntési szituációi, stratégiái különben sem kötődnek egy-egy konkrét programozási nyelvhez. Természetesen legalább egy konkrét nyelven meg kell valósítani a megoldásokat, és ez a nyelv ebben a kötetben a C++ lesz, de szeretném, ha a kötet tanácsait más nyelveken történő megvalósítás esetén is fel lehetne használni. Ennek a kötetnek az első része a programtervező informatikus szak Programozási alapismeretek tárgyához nyújt közvetlen segítséget, második része és a harmadik rész első két fejezete a Programozás tantárgy implementációról szóló ismeretanyagát fogja át, utolsó három fejezete pedig 8

9 az Objektumelvű alkalmazások fejlesztése tantárgyhoz kapcsolódik. A kötetben szereplő mintapéldák egy része a képzésünkben már megszűnt Alkalmazások fejlesztése I. és II. című tantárgyból származik. Ennek kapcsán feltétlenül meg kell említenem Szabóné Nacsa Rozália, Sike Sándor, Steingart Ferenc és Porkoláb Zoltán nevét, akik az említett elődtantárgynak a kidolgozásában részt vettek, és közvetve hozzájárultak ennek a kötetnek a megszületéséhez. Ez a kötet akárcsak az első gyakorlatorientált. Negyvenkét feladat megoldásának részletes bemutatása található benne, amely kiegészül a szükséges ismeretek (implementációs stratégiák, nyelvi eszközök) leírásával. A kötetben külön gyakorló feladatok nincsenek, gyakorlásként az első, a tervezésről szóló kötet feladatainak megoldási tervét lehet megvalósítani. A két kötetet (ezt a megvalósításról szólót illetve a tervezésről szóló első kötetet) úgy terveztem, hogy ezek egymástól függetlenül is érthetőek legyenek, de az ajánlott olvasási sorrend az, hogy a kötetek egyes részeit párban dolgozzák fel a hallgatók. Az első kötet első részében az alapvető programozási fogalmakat vezetem be. Ennek megismerése után ennek a kötetnek az első részét érdemes áttekinteni, amely nagyon egyszerű programok készítését, és azokhoz szükséges nyelvi elemeket mutatja be. Az első kötet második részében a visszavezetésre épülő programtervezési technikát találjuk, míg itt a második rész az ilyen módon előállított programtervek megvalósításáról szól. A harmadik rész mindkét kötetben a korszerű típus fogalmához kapcsolódik. Az első kötetben a típus-központú tervezésről, ebben a kötetben a felhasználói típusok osztályokkal történő megvalósításáról olvashatunk, azaz az objektum-orientált programozás alapjaival ismerkedhetünk meg. A jegyzet tananyagának kialakítása az Európai Unió támogatásával, az Európai Szociális Alap társfinanszírozásával valósult meg (a támogatás száma TÁMOP /B-09/1/KMR ). 9

10 BEVEZETÉS A programkészítés egy olyan folyamat, amely magába foglalja a feladat elemzését, a megoldás tervezését, a programkód előállítását, majd tesztelését és végül a dokumentálást. Ezek sorrendjét mutatja be a szoftver előállításának hagyományos, úgynevezett vízesés modellje (1.ábra). A valóságban egy program elkészítése nem ennyire szabályos mederben folyik. Minél összetettebb ugyanis a megoldandó probléma, annál többször fordul elő, hogy a megoldás előállításához vezető út egyik vagy másik korábban elvégzett szakaszához vissza kell térni, azt újra kell gondolni, és ennek következtében az azt követő szakaszokat (még ha nem is teljes egészében) meg kell ismételni. A vízesés modell nem azt mutatja meg, hogy hány lépésben áll elő a megoldás, hanem csak azt, hogy milyen természetű kérdésekkel kell foglalkozni a programkészítés során, és az egyes szakaszokban hozott döntések mely más szakaszokra vannak hatással. Feladat Elemzés Specifikáció Tervezés Programterv Megvalósítás Programkód Tesztelés, Dokumentálás Megoldás 1.ábra Egyszerűsített vízesés modell 10

11 A modell például egyértelműen jelzi, hogy egy probléma megoldásánál először a probléma elemzésével kell foglalkozni (mi a feladat, milyen adatokat ismerünk, mit kell kiszámolnunk, azaz mi a cél, milyen formában állítsuk elő a választ, milyen eszközök állnak rendelkezésünkre a megvalósításhoz, stb.). Ezt követően lehet a megoldó program logikáját, absztrakt vázát megtervezni. Ha tervezés során kiderül, hogy a feladat nem teljesen világos, akkor vissza kell térni a feladat elemzéséhez. Csak a terv (még ha ez nem is végleges) birtokában kezdhetjük el a program adott programozási nyelven történő implementálását, majd az így kapott programkód ismételt futtatásaival végezhető el a megoldás módszeres kipróbálása, a tesztelés. Nem megfelelő teszteredmények esetén a megelőző lépések valamelyikét kell korrigálni, amely több más lépés javítását is kiválthatja. Sohasem szabad azonban szem elől téveszteni azt, hogy az adott pillanatban éppen melyik szakaszával foglalkozunk a megoldásnak. Nem vezetne célhoz ugyanis, ha például a téves tervezés miatt rosszul működő programnak a kódját módosítanánk a helyett, hogy a tervet vizsgálnánk felül. Végül nem szabad megfeledkeznünk a megoldás dokumentálásáról sem, hiszen egy programkód akkor válik termékké, ha annak használatát, karbantartását, továbbfejlesztését mások számára is érthető módon leírjuk. Sokszor hallani azt az érvet, hogy egy egyszerű feladat megoldásánál nincs szükség a tervezésre. Ez nem igaz! Az lehet, hogy egy egyszerű feladatot megoldó program esetében elég, ha a megoldás terve csak a programozó fejében ölt testet. De terv ekkor is van, és a programozás tanulása során eleinte ezt az egyszerű tervet sem árt írásban rögzíteni. A terv magába foglalja a feladat elemzésének eredményét, a feladat adatait, az adatok típusát, változóik nevét, azt, hogy ezek között melyek a bemenő illetve eredményt hordozó változók, a bemenő változók milyen előfeltételeknek tesznek eleget, egy szóval a feladat specifikációját. Az eredményváltozókra megfogalmazott úgynevezett utófeltétel is a specifikáció része, de erre az implementálásnál közvetlenül már nincs szükség (az utófeltétel az absztrakt megoldás előállítását, illetve a program tesztelését segíti), hanem helyette a megoldó program absztrakt vázát például struktogrammját kell ismernünk. A terv tehát tartalmazza a megoldás absztrakt, a konkrét programozási környezettől (számítógéptől, 11

12 operációs rendszertől, programozási nyelvtől, fejlesztő környezettől) elvonatkoztatott változatát. A megoldás tervéből a megvalósítás (implementálás) során készül el a működő program. A megvalósításnak az eszköze az a programozási nyelv és programozási környezet (számítógép, operációs rendszer, fejlesztő eszközök), amelyben a programot fejlesztjük. Az implementálás hangsúlyos része a kódolás, amikor az absztrakt program utasításait a konkrét programnyelv utasításaira írjuk át, és ehhez nélkülözhetetlen a konkrét programozási nyelv ismerete. A programkód elkészítése azonban nem pusztán a tervezés során előállt absztrakt program mechanikus lefordításából (kódolásából) áll. Az implementálás számos olyan döntést és tevékenységet is magába foglal, amely a programozási nyelvtől független. Például konzolos alkalmazásoknál, amelyek kódolásával ebben a kötetben foglalkozunk, a terv általában nem tér ki arra, hogy a bemenő változók hogyan vegyék fel a kezdőértékeiket, illetve az eredményváltozók értékeit hogyan tudjuk a felhasználó felé láthatóvá tenni. Pedig ezekről a kérdésekről, az adatok beolvasásáról és az eredmény kiírásáról a programnak gondoskodnia kell. A program előállításakor a feladat megoldási tervén túlmutató számottevő kódot kell tehát előállítanunk, ezért nevezzük ezt a folyamatot a terv kódolása helyett a terv implementálásának. Ugyancsak túlmutat a mechanikus kódoláson az, amikor figyelembe kell venni az olyan nem funkcionális követelményeket, mint például a memóriaigényre vagy futási időre szabott feltételek. A hatékonyságra ugyan már a tervezési fázisban lehet és kell figyelni, de a kódoláskor a megfelelő nyelvi eszközök kiválasztásával is jelentősen lehet javítani a program memóriaigényét és futási idejét. De ne felejtsük el, hogy egy rosszul működő program hatékonyságával értelmetlen dolog foglalkozni; a hatékonyság kérdése csak akkor kerül előtérbe, ha a program már képes megoldani a feladatot. Ebben a könyvben programozási nyelvnek a C++ nyelvet választottam, hiszen azoknál a tantárgyaknál, amelyeknek jegyzetéül szolgál ez a kötet, ugyancsak a C++ nyelvet használjuk. A fejezetekben látszólag sok C++ nyelvi elemet mutatok be (eleinte sok apró elemet, később egyre kevesebbet, bár súlyát tekintve nagyobb horderejűt), mégsem e nyelvi elemek bemutatásán van a hangsúly. A C++ nyelv csak illusztrálásul szolgál az implementáláshoz, ezért nem is törekszem a teljes megismertetésére. Nem foglalkozok annak 12

13 megmutatásával sem, hogy pontosan mi történik egy C++ utasítás végrehajtáskor a számítógépen, ha annak megértését és felhasználhatóságát valamilyen egyszerűbb modell segítségével is érthetővé lehet tenni. Első sorban azokra a kérdésekre koncentrálok, amelyek az implementáció során vetődnek fel. Egy programozási nyelvnek az alapszintű megismeréséhez ha ez nem az első programozási nyelv, amivel találkozunk néhány óra is elegendő. Ez azért van így, mert egy gyakorlott programozó már tudja, hogy melyek egy nyelv azon elemei, amelyeket mindenképpen meg kell ismerni ahhoz, hogy egy program kódolását elkezdhessük. Megpróbálom a C++ nyelvet ilyen szemmel bemutatni: mindig csak egy kis részt vizsgálok meg belőle, éppen annyit, amelyet feltétlenül ismerni kell, hogy a soron következő programot el lehessen készíteni. Nem vérbeli C++ programozók képzése a célom, hanem olyanoké, akik meg tudnak oldani egy programozási feladatot jobb híján C++ nyelven. De vajon lehetséges egyáltalán kódolásról beszélni a programozási nyelv alapos ismerete nélkül. A válasz: igen. Ahhoz tudnám hasonlítani ezt a helyzetet, mint amikor valakinek egy idegen nyelvet kell megtanulnia. Ezt lehet az adott nyelv nyelvtanának tanulmányozásával kezdeni (különösen, ha van már ismeretünk egy másik, mondjuk az anyanyelvünk nyelvtanáról), vagy egyszerűen csak megpróbáljuk használni a nyelvet (anélkül tanulunk meg egy kifejezést, hogy pontosan tudnánk, az hány szóból áll, melyik az alany, melyik az állítmány). Ebben a kötetben ezt a második utat követem, a feladat megoldását helyezem középpontba, és a C++ nyelvről csak ott és éppen annyit mondok el, amire feltétlenül szükségünk lesz. Értelemszerűen nem foglalkozok a hatékonyság növelésének nyelvi sajátosságaival, hiszen ez a nyelv haladó szintű ismeretét igényli, de igyekeztem minden helyzetre a legalkalmasabb nyelvi eszközt megtalálni: olyat, amit a szakma támogat, amelynek a használata biztonságos és hatékony, a leírása egyszerű, működésének magyarázata nem igényel mély operációs rendszerbeli ismereteket, és kellően általános ahhoz, hogy hasonló nyelvi elemet más nyelvekben is találjunk. A mondanivalóm szempontjából a fejlesztő eszköz megválasztása még annyira sem lényeges, mint a programozási nyelvé. Célszerű a kezdő programozóknak úgynevezett integrált fejlesztő környezetet használniuk, amely lehetőséget ad minden olyan tevékenység végzésére, amely a 13

14 programozásnál szükséges. Támogatja a kód (C++ nyelven történő) szerkesztését, a kódnak a számítógép nyelvére történő lefordítását (itt az adott nyelv szabványos fordítóprogramját célszerű használni), a fordításnál esetlegesen jelzett alaki (szintaktikai) hibák helyének könnyű megtalálását, a futtatható gépi kódú program összeszerkesztését, a program futtatását, tesztelését, valamint a tartalmi (szemantikai) hibák nyomkövetéssel történő keresését. Alkalmazásainkat minden esetben egy úgynevezett projektbe ágyazzuk be. Eleinte, amíg az alkalmazásunk egyetlen forrás állományból áll, talán körülményeskedőnek hat ez a lépés, de ha megszokjuk, akkor később, a több állományra tördelt alkalmazások készítése is egyszerű lesz. A programok kódolásánál törekedjünk arra, hogy a beírt kódot olyan hamar ellenőrizzük fordítás és futtatás segítségével, amilyen hamar csak lehet. Ne a teljes kód beírása után fordítsuk le először a programot, mert a hibaüzenetek tényleges okát ekkor már sokkal nehezebb megtalálni! Eleinte ne szégyelljük, ha utasításonként fordítunk, de később se írjunk le egy program-blokknál többet fordítás nélkül. A bizonyítottan helyes absztrakt program kódolása többnyire nem eredményez működő programot. A kódolás során ugyanis elkövethetünk alaki (szintaktikai) hibákat (ezt ellenőrzi fordításkor a fordító program) és tartalmi (szemantikai) hibákat. Ez utóbbiak egy részét a kódolási megállapodások betartásával tudjuk kivédeni, más részét teszteléssel felfedezni, a hiba okát pedig nyomkövetéssel megtalálni. A fordításnál keletkezett hibaüzenetek értelmezése nem könnyű. A fordítóprogram annál a sornál jelez hibát, ahol a hibát észlelte, de a hiba gyakran több sorral előbb található vagy éppen nem található (ilyen például egy változó deklarálásának hiánya). Érdemes a hibaüzeneteket sorban értelmezni és kijavítani, mert bizonyos hibák egy korábbi hibának a következményei. Fontos tudni, hogy a hibaüzenet nem adja meg a hiba javításának módját. Nem szabad a hibaüzenet által sugallt javítást azonnal végrehajtani, hanem először rá kell jönnünk a hiba valódi okára, majd meg kell keresnünk, hol és mi módon korrigálhatjuk azt. Ne csak a fordító hibaüzeneteit (error), hanem a figyelmeztetéseit (warning) is olvassuk el. A figyelmeztetések rámutathatnak más, nehezen értelmezhető hibaüzenetek 14

15 okaira, vagy később fellépő rejtett hibákra. A szerkesztési hibák egy része elkerülhető a kódolási megállapodások betartásával. Érdemes a kódot úgy elkészíteni, hogy annak bizonyos részei minél hamarabb futtathatók legyenek. Így a programot már akkor ki tudjuk próbálni, amikor az még nem a teljes feladatot oldja meg. Ne sajnáljuk a tesztelésre szánt időt! Gondoljuk át milyen tesztesetekre (jellegzetes bemenő adatokra) érdemes kipróbálni a programunkat. A fekete doboz teszteseteket a feladat szempontjából lényeges vagy extrém (szélsőséges) bemenő adatok és a várt eredmények adják, külön választva ezek közt a feladat előfeltételét kielégítő, úgynevezett érvényes, és az azon kívül eső érvénytelen tesztadatokat. A fehér doboz teszteseteket a program kód ismeretében generált tesztadatok alkotják, amelyek együttesen biztosítják, hogy a kód minden utasítása ki legyen próbálva, valamint az összefutó és elágazó vezérlési szálak minden lehetséges végrehajtására sor kerüljön, azaz az úgynevezett szétágazási és gyűjtőpontjainál mindenféle irányban legalább egyszer keresztülmenjen a vezérlés. A tesztelést (akárcsak a fordítást, futtatást) menet közben, egy-egy részprogramra is érdemes már végrehajtani. A nyomkövetés egy tartalmi hiba bekövetkezésének pontos helyét segít kideríteni, noha ez nem feltétlenül az a hely, ahol programot javítani kell. A nyomkövetés a program lépésenkénti végrehajtása, melynek során például megvizsgálhatjuk a program változóinak pillanatnyi értékét. Egy program dokumentálása alapvetően két részből áll. A felhasználói leírás az alkalmazás használatát mutatja be, arról a programot használóknak ad felvilágosítást. Tartalmazza a megoldott feladat leírását, a program működésének bemutatását néhány tipikus használati esetre (bemenő adatkombinációra), a program működéséhez szükséges hardver és szoftver feltételeket, és a program működtetésének módját (telepítés, futtatás). A fejlesztői leírás a megoldás szerkezetébe enged bepillantást. Elsősorban programozóknak szól, és lehetővé teszi, hogy a programot ha szükséges könnyebben lehessen javítani, módosítani. Tartalmazza a feladat szövege mellett a feladat specifikációját, a program tervét, az implementáció során hozott, a tervben nem szerepelő döntéseket (beolvasási, kiírási szakasz megvalósítása, hatékonyság javítása érdekében végzett módosítások), a 15

16 programkódot (összetett program esetén a részeinek egymáshoz való kapcsolódását), a tesztelésnél vizsgált teszteseteket. Ezen kívül esetleges továbbfejlesztési lehetőségeket is megemlíthet. Ebben a kötetben a feladatok megoldásánál lényegében ez utóbbit, a fejlesztői leírást fogjuk megadni úgy, hogy kiegészítjük azt a megértést segítő megjegyzésekkel. Felhasználói leírást a feladatok egyszerű voltára tekintettel nem adunk. A kötetben negyvenkettő programozási feladat megoldása található. A megoldásokat tartalmazó fejezeteket úgy alakítottam ki, hogy mindegyik elején először az érintett feladatok megoldásához köthető implementációs stratégiákat ismertetem, majd bemutatom a szükséges nyelvi elemeket és kódolási konvenciókat. A fejezetek ezeket az ismereteket fokozatosan, egymásra épülő sorrendben vezetik be. A fejezeteket az azokban először megjelenő C++ nyelvi elemek összefoglalása zárja le. A kötet három részből áll. Az első rész nagyon egyszerű programok készítésén keresztül illusztrálja az implementáció folyamatát. Megmutatja, hogyan kell egy struktogrammot kódolni, hogyan készülnek a programok beolvasást illetve kiírást végző részei, ha erre a konzolt vagy szöveges állományt használunk. Megismerjük a tömbök készítésének és használatának módját. A második rész az alprogramokra (függvényekre, eljárásokra) tördelt programok készítéséről szól. Először csak az alprogramok használatáról, majd az első kötetben tárgyalt programozási tételekből származtatott kódok alprogramba ágyazásáról, ezt követően a több programozási tétellel megoldott feladat programjának több alprogramra bontásáról, és az alprogramok csoportjainak külön fordítási egységben, külön komponensben történő elhelyezéséről, és végül a rekurzívan hívott alprogramokról lesz szó. A harmadik rész az osztályokat használó programokat mutatja be. Először az egyszerű osztály fogalmát ismerjük majd meg néhány, az első kötetben tárgyalt típus megvalósításán keresztül. Majd különféle felsorolók megvalósításával az első kötet harmadik részének megoldásait implementálhatjuk. Ez után dinamikus adatszerkezetű típusok osztályait implementáljuk. Végül megismerjük az osztály-származtatás és a sablonpéldányosítás eszközét, és ezek segítségével példákat mutatunk objektum orientált stílusú megvalósításokra. 16

17 I. RÉSZ ALAPOK Ebben a részben egyszerű, kisméretű, egyetlen programblokkban kódolható konzolalkalmazások készítését vehetjük szemügyre, és ezen keresztül megfigyelhetjük a programkészítés folyamatát. Egy egyszerű konzolalkalmazás esetében a kód három szakaszra tagolódik: bemenő adatok beolvasására és ellenőrzésére; a számítások elvégzésére; az eredmény megjelenítésére. Ez a felosztás a nagyobb (de nem objektum orientált) konzolalkalmazásoknál is megfigyelhető. A számítási szakasz az absztrakt algoritmusnak megfelelő kódot tartalmazza. Amennyiben az absztrakt program strukturált, akkor annak szerkezete alapján kívülről befelé haladó kézi fordítással állítjuk elő a kódot. Mindig az adott szerkezeti elemnek (szekvencia, elágazás, ciklus) megfelelő utasítást írjuk le, és amikor elemi utasításhoz érünk, azt közvetlenül kódoljuk. A beolvasási szakaszban gondoskodnunk kell arról, hogy a bemenő változók megfelelő kezdőértéket kapjanak. A feladat tervéből kiderül, hogy programunknak melyek a bemenő változói, és azok kezdő értékeire milyen feltételnek kell teljesülnie. A bemenő adatok kezdőértékeit többnyire a szabványos bemenetről vagy egy szöveges állományból nyerjük. A beolvasott adatokra meg kell vizsgálni, teljesítik-e a szükséges előfeltételeket. Ha nem, akkor hiba-jelzést kell adni, és vagy leállítani a programot, vagy újra meg kell kísérelni a beolvasást. Előfordulhat, hogy a bemenő adatok beolvasása a számítási szakasszal összeolvad, mert az adatokat olyan ütemezéssel kell beolvasni, ahogy azt a számítás igényli. Az eredmény megjelenítése az eredményváltozók tartalmának képernyőre, vagy más, a felhasználó által olvasható adathordozóra történő írásából áll. Itt is előfordulhat, hogy ez a szakasz összeolvad a számítási szakasszal. Melyek egy programozási nyelvnek azon részei, amelyeket okvetlenül meg kell ismernünk az egyszerű feladatok implementáláshoz? Valójában igen csekély tudás birtokában már fel tudunk építeni egy egyszerű programot. 17

18 1. A programkód szerkezete 2. Típusok, kifejezések, változók, értékadás 3. Vezérlési szerkezetek 4. Input-output 5. Könyvtári elemek I-1.ábra. Amit egy programozási nyelvről mindenképpen meg kell tanulnunk Mindenekelőtt meg kell tanulnunk, hogy az adott nyelven milyen szerkezeti keretben lehet a kódjainkat elhelyezni. Ez a szerkezet általában összetett, de szerencsére az első, egyszerű programok elkészítéséhez nem kell még azt a maga teljességében átlátni. Már a kezdeteknél tisztázni kell, hogy milyen alaptípusokat (pl. egész, valós, logikai, karakter) és milyen típusösszetételeket (pl. tömb) ismer az adott nyelv, hogyan kell leírni e típusok értékeit, hogyan lehet ilyen típusú változókat deklarálni, milyen műveletek végezhetők ezekkel, milyen kifejezések építhetők fel belőlük. Az absztrakt program kódolásához a vezérlési szerkezetek nyelvi megfelelőit kell megismernünk. Eleinte elég a három vezérlési szerkezetnek általánosan megfelelő nyelvi elemeket megtalálni, később meg lehet azt vizsgálni, hogy egy vezérlési szerkezetet speciális esetben milyen sajátos nyelvi elemmel lehet még kódolni (pl. mikor használjunk for ciklust a while ciklus helyett, switch-t az if-else-if helyett). Talán a nyelvek legegyedibb része az, ahogyan a környezetükkel való kapcsolatot megvalósítják, azaz az input-output utasításai. Az input-output általában összetett, de mi csak két területével foglalkozunk: a konzolablakos input-outputtal és a szöveges állományokkal. Egy nyelv erejét mutatja, hogy támogatja-e korábban megírt kódrészek újrafelhasználását. Vannak-e olyan úgynevezett kódkönyvtárak, amelyeket szükség esetén a programunkhoz csatolhatunk azért, hogy azok elemeit (speciális típusokat, objektumokat, függvényeket) felhasználhassuk. Látni 18

19 fogjuk, hogy már a legegyszerűbb programok írásánál is szükség lehet ilyen könyvtári elemekre. 19

20 1. Első lépések Ebben a fejezetben nagyon egyszerű (egyszerű programtervvel rendelkező) feladatok megoldásán keresztül mutatjuk be a programkészítésnek azt a szakaszát, amelyet megvalósításnak vagy más néven implementálásnak neveznek. Ezzel párhuzamosan megismerkedünk a C++ programozási nyelv néhány alapvető nyelvi elemével: az egész számok típusával, változók deklarálásának módjával, az értékadás utasítással és a szabványos inputoutput műveletekkel. Implementációs stratégia Az egyszerű (úgynevezett konzolablakos) megoldások implementálását öt lépésben végezzük el. Természetesen ezeket a lépéseket nem kell feltétlenül az itt megadott sorrendben, egymástól szigorúan különválasztva végrehajtani, de mindig fontos tudni azt, hogy a program kódnak éppen melyik részét készítjük, ugyanis ettől függ, hogy milyen tanácsokat és nyelvi elemeket vehetünk igénybe. Az egyes lépések elvégzésénél (az első kivételével) a programtervre támaszkodhatunk. 1. Programkód keretének elkészítése 2. Absztrakt program kódolása 3. Bemenő adatok beolvasása és ellenőrzése 4. Eredmény megjelenítése 5. Változók deklarálása 1-1. ábra. Egyszerű programok implementálásának lépései Egy program kódját a választott programozási nyelvre jellemző keretbe kell befoglalni. Ehhez a programozási nyelv azon speciális nyelvi elemeit, utasításait kell felhasználni, amelyek kijelölik a programkód elejét és végét, meghatározzák, hogy melyik utasítás végrehajtásával kezdődjön a program 20

21 futása, továbbá különleges előírásokat adhatnak a programkód lefordításához. Ezen nyelvi elemek közé, mint egy keretbe kell majd a kód többi részét beilleszteni. A kódnak lényeges részét alkotja a programtervben rögzített absztrakt programnak az adott programozási nyelvre lefordított változata. Az implementáció e második lépése a szó szoros értelemben vett kódolás, amikor a programot egy absztrakt nyelvről (pl. struktogramm leírás) egy konkrét nyelvre (mondjuk: C++) fordítjuk. Ehhez azt kell tudni, hogy az absztrakt program utasításainak a programozási nyelv mely utasításai felelnek meg. Egy utasítás lehet egyszerű: üres lépés vagy értékadás, de lehet összetett: egy vezérlési szerkezet (szekvencia, elágazás, ciklus). Előfordulhat, hogy az absztrakt program leírása olyan elemi utasítást tartalmaz, amelyet nem lehet közvetlenül kódolni a választott konkrét nyelven. Ilyen lehet például egy szimultán értékadás, amelyik egyszerre, egy időben ad több változónak is új értéket. Ilyenkor a megvalósítás során a programtervet kell finomítani, részletezni. A megvalósítás harmadik és negyedik lépésében az absztrakt program és a futtató környezet közötti kapcsolatot megteremtő kódot kell kitalálni. Ennek segítségével a környezetből eljutnak a bemenő adatok a programhoz, és az eredmény visszajut a környezetbe. Ennek a kódnak általában nincs absztrakt változata, de a feladat specifikációja kijelöli azokat a változókat, amelyek kezdő értékeit be kell olvasni a felhasználótól, és azokat, amelyek értékét meg kell jeleníteni a felhasználó számára. Ezekben a lépésekben tehát nem abban az értelemben vett kódolás zajlik, hogy egy adott programot kell egy másik nyelvre átírni, hanem egy sokkal kreatívabb tevékenység: létre kell hozni olyan kódrészleteket, amelyhez korábbi tapasztalataink, kódmintáink szolgálhatnak segítségül. A specifikációban rögzített állapottér megmutatja a bemenő változók típusát, a specifikáció előfeltétele pedig rögzíti, hogy a bemenő adatoknak milyen feltételeket kell kielégítenie. Ezek együttesen meghatározzák a beolvasás módját. A bemenő adatok beolvasásához olyan kódot kell készítenünk, amelyik megkísérli a kívánt típusú érték beolvasását és ellenőrzi a szükséges feltételeket. Ha nem sikerül a megfelelő értéket beolvasni, akkor a kódnak meg kell akadályozni a program további futását. Ehhez kétféle 21

22 stratégia közül választhatunk. Az egyik megfelelő hibaüzenet generálása után leállítja a program futását, a másik újra és újra megpróbálja beolvasni az adatot, amíg az helyes nem lesz. Azt, hogy ezek közül melyiket alkalmazzuk, a konkrét feladat kitűzése illetve a beolvasandó adat forrása határozza meg. Például, ha az adatokat közvetlenül a felhasználótól kapjuk, akkor alkalmazható a második módszer, de ha az adatok egy korábban feltöltött szöveges állományból származnak, akkor csak az első út járható. A kiírás figyelembe veszi az eredmény-változók típusát (ez az állapottérből olvasható ki), valamint a specifikáció utófeltételét. Gyakori eset például az, hogy egy eredmény-változó csak bizonyos feltétel teljesülése esetén kap értéket. Ilyenkor a kiírás kódja egy elágazást fog tartalmazni, amely csak megadott esetben írja ki az eredményt, egyébként pedig egy tájékoztatást ad annak okáról, hogy az eredmény miért nem jelenhet meg. Bemenő változó: Az absztrakt program olyan változója, amely az absztrakt program végrehajtása előtt rendelkezik (a megoldandó feladat előfeltételének) megfelelő kezdőértékkel. Eredmény-változó: Az absztrakt program olyan változója, amely az absztrakt program (tervezés által garantált) befejeződése után a (megoldandó feladat utófeltétele által) kívánt értéket tartalmazza. Segédváltozó: A program működése közben létrejött változó ábra. Program változói tervezési szempontból A változók fontos szereplői a programoknak. Ezek java részét a tervezés során vezetjük be, hiszen már ott kiderülnek a megoldandó feladat bemenő- és eredmény-változói, valamint számos segédváltozó is, de az implementáláskor még további segédváltozók is megjelenhetnek. A változók bevezetése a tervezés szintjén a változók típusának megadásával jár együtt. A programban használt változókat (a feladat bemenő- és eredmény-változóit, 22

23 az absztrakt program segédváltozóit, és az implementálás során bevezetett egyéb segédváltozókat) többnyire (C++-ban mindig) deklarálni kell, azaz explicit módon meg kell adni a típusukat. Ehhez ismerni kell az adott nyelvben a deklaráció formáját, elhelyezésének módját, a deklarációnál használható alaptípusokat és azok tulajdonságait (típusértékeit és műveleteit). Vannak azonban olyan programozási nyelvek is, ahol a változó első értékadása az értékül adott kifejezés típusával deklarálja a változó típusát. A változók deklarációja a megvalósítás ötödik lépése. Nyelvi elemek Egy program kódjának sorait az úgynevezett forrásállományba írjuk bele, amelyet bármilyen szöveges állományt előállító szövegszerkesztővel elkészíthetünk. Ennek az állománynak általában valamilyen speciális, az adott programozási nyelvre utaló kiterjesztése van. A C++ nyelvű programjainkat.cpp kiterjesztésű állományokba tesszük. (A gyakorlatban ezen kívül gyakran előfordul a.c,.c,.cxx kiterjesztés is.) Az egyszerű programok egyetlen forrásállományban helyezkednek el, az összetett programok kódját több állományba lehet szétosztani. A forrásállományokban elhelyezett kódban egyértelműen kell megjelölni azt az utasítást, amelynél a program végrehajtása (futása) elkezdődik. C++ nyelven ez az úgynevezett main függvény blokkjának első utasítása. Ezt a blokkot a main függvény nevét követő nyitó és csukó kapcsos-zárójelek határolják. A kerethez tartoznak a kód olyan bejegyzései, amelyek a fordító program számára hordoznak üzenetet. Ilyen például azon kódkönyvtárak kijelölése, amelynek elemeit fel szeretnénk használni, ezért a kódkönyvtárat be akarjuk másolni (include) a programunkba. A konzolos input-output tevékenységekhez például szükségünk lesz egy #include <iostream> sorra. Itt kell megemlíteni a minden forrásállomány elején kötelezően ajánlott using namespace std utasítást is. Számos sokszor használt előredefiniált azonosító ugyanis az úgynevezett standard (std) névtérben található. Ez azt jelenti, hogy ezek az előre definiált elemek egy olyan csomagban találhatók, amelynek elemeire az std:: előtag (úgynevezett 23

24 minősítés) segítségével lehetne csak hivatkozni, ha nem használnánk a using namespace std utasítást. C++ nyelven a main függvényben elhelyezett return utasítás a program futását állítja le. A main függvény előtti int (integer) szócska utal arra, hogy a return után egy egész számot (egész értékű kifejezést) kell írni. Ezt az értéket a program leállása után a futtató környezet (az operációs rendszer) kapja meg, és tetszés szerint reagálhat rá. Ha ez nulla, akkor az a minden rendben üzenetet szimbolizálja. A return kifejezés helyett használható még az exit(kifejezés) is, de ehhez bizonyos környezetekben be kell illeszteni a program elejére egy #include <cstdlib> sort. A változó deklarációja a változó típusát írja le. A típus határozza meg, hogy egy változó milyen értékeket vehet fel és azokkal milyen műveletek végezhetők. Egy futó program változójához a típusán kívül hozzátartozik a számítógép memóriájának azon része is, ahol a változó aktuális értéke tárolódik. A memóriára első megközelítésben gondoljunk úgy, mint bájtok (1 bájt = 8 darab 0 vagy 1-est tároló bit) sorozatára. Minden bájtot egyértelműen azonosít az ebben a sorozatban elfoglalt pozíciója, azaz sorszáma, amit a bájt címének hívunk. A sorszámozást nullával kezdjük. Számok bináris alakja A számítógépek memóriájában a számokat bináris alakban (kettes számrendszerben) ábrázoljuk. A bináris alakban felírt szám egy számjegye 0 vagy 1 értékű lehet. Ezt hívjuk bitnek. A számjegy értéke a pozíciójától, a helyiértéktől függ. A kettes számrendszer helyiértékei a kettő hatványai. Egy természetes szám bináris alakjának számjegyeit helyiérték szerint növekvő sorrendben egy kettővel való osztogatásos módszerrel lehet előállítani. Az osztásoknál keletkezett maradék a soron következő számjegy, az osztás eredménye pedig a következő osztás osztandója. Az osztogatás addig tart, amíg az osztandó nem nulla. Példa: Adjuk meg a 183 (10) bináris alakját! 183 : 2 = 91 maradt: 1 91 : 2 = 45 maradt: 1 24

25 45 : 2 = 22 maradt: 1 22 : 2 = 11 maradt: 0 11 : 2 = 5 maradt: 1 5 : 2 = 2 maradt: 1 2 : 2 = 1 maradt: 0 1 : 2 = 0 maradt: 1 Tehát 183 (10) = (2) = 1* * * * * * * *2 0 Egy törtszám bináris alakjának számjegyeit helyiérték szerint csökkenő sorrendben kettővel való szorzásos módszerrel lehet előállítani. A szorzat egész része a soron következő számjegy, törtrésze a következő szorzás szorzandója. A módszer addig tart, amíg a szorzandó nem nulla. Előfordulhat, hogy ez nem következik be, mert a kettes számrendszerben felírt szám egy végtelen kettedes tört lesz. Példa: Adjuk meg a (10) bináris alakját! * 2 = egészrész: 1 törtrész: * 2 = egészrész: 1 törtrész: * 2 = 0.50 egészrész: 0 törtrész: * 2 = 1.0 egészrész: 1 törtrész: 0 Tehát (10) = (2) = 1*1/2 + 1*1/4 + 0*1/8 + 1*1/16 Példa: Adjuk meg a 0.1 (10) bináris alakját! 0.1 * 2 = 0.2 egészrész: 0 törtrész: * 2 = 0.4 egészrész: 0 törtrész: * 2 = 0.8 egészrész: 0 törtrész: * 2 = 1.6 egészrész: 1 törtrész: * 2 = 1.2 egészrész: 1 törtrész: * 2 = 0.4 egészrész: 0 törtrész: * 2 = 0.8 egészrész: 0 törtrész: 0.8 Tehát 0.1 (10) = (2) (végtelen szakaszos tört) 25

26 Egy változónak csak azt követően lehet értéket adni, hogy kijelöltük a memóriában a helyét, azaz megtörtént a változó helyfoglalása. Ezt nevezzük a változó definíciójának. Az értéket bináris kódolással egy vagy több egymás utáni bájton tároljuk. A tároláshoz szükséges memória szelet első bájtjának címe a változó címe, a tároláshoz használt bájtok száma a változó mérete. változó regisztráció név típus cím lefoglalt memória szelet érték 1-3. ábra. Változó jellemzői Az erősen típusos nyelvekben egy változónak mindig van típusa, de neve, memória címe és értéke nem feltételenül. A változó deklarációja és a helyfoglalása közötti időben a változó nem rendelkezik még memória területtel, tehát sem címe, sem értéke nincs. A memóriafoglalás után, a változónak már lesz értéke, mert a kijelölt memória szelet eleve tartalmaz 0 vagy 1-es biteket. Az ezekből kiszámolt érték azonban előre nem ismert, bizonytalan, másképpen fogalmazva nem definiált, amit úgy is felfoghatunk, hogy a változó még nem kapott kezdeti értéket. (Találkozhatunk olyan változókkal is, amelyeknek hiányzik a neve, az értékükre csak a címük segítségével hivatkozhatunk.) C++ nyelven a változó neve (csak úgy, mint más azonosítóké) betűk és számok sorozatából áll. Az első karakternek mindig betűnek vagy _ (aláhúzás) jelnek kell lennie, és nem használhatóak változónévként a nyelv foglalt kulcsszavai. A változókkal kapcsolatos alapvető utasítás az értékadás. Egy kicsit megtévesztő, hogy az absztrakt programokban (és a Pascal nyelven) változó:=kifejezés formában felírt értékadást a C-szerű nyelvekben a változó = kifejezés utasítás írja le, azaz nincs benne az értékadás irányát jelző kettőspont. A kifejezés olyan változókból, típusértéket megjelenítő szimbólumokból (úgynevezett literálokból), műveleti jelekből és függvények 26

27 visszaadott értékeiből álló formula, amely megfelel bizonyos alaki és jelentésbeli szabályoknak. Alaki (szintaktikai) hiba, például ha egy osztás műveleti jelhez csak egy argumentumot írunk (/8.2), jelentésbeli (szemantikai) pedig, például ha két karakterláncot akarunk összeszorozni ( füzér * lánc ), feltéve, hogy nem definiáltuk két lánc szorzását. Előfordulhat olyan eset is, amikor a kifejezés helyes, de mégsem lehet az értékét kiszámolni, a program abortál. Gondoljunk például az x/y osztásra, ha az y változó értéke nulla. A konzolos input-output műveletek alapszinten lehetőséget adnak arra, hogy egy bemenő változó a billentyűzetről értéket kaphasson, illetve egy eredmény-változó vagy eredmény-kifejezés értékét a konzol ablakban megjeleníthessük. A kódban elhelyezett beolvasás utasításnak tartalmaznia kell, hogy melyik változónak akarunk értéket adni, a kiíró utasításnak pedig azt a kifejezést, amelynek az értékét meg akarjuk jeleníteni. Az értékek a felhasználó számára karakterek sorozatával írhatóak le. Például egy egész szám egy előjelből (-/+), majd néhány számjegyből áll. A beolvasás során ezeket a karaktereket a megfelelő billentyűk lenyomásával adjuk meg, és az <enter> billentyűvel fejezzük be az érték leírását. A begépelt karaktersorozat a konzolablakban is látható. Kiíráskor az értéket leíró karakterek sorozata fog a konzol ablakban megjelenni. C++-ban a beolvasás és a kiírás legegyszerűbb formája az úgynevezett szabványos input- illetve output adatfolyamok használata. A cin (consol input) objektumon keresztül a szabványos bemeneten (ez lehet a billentyűzet) keletkező jeleket (karaktereket) tudjuk beolvasni és adatokká csoportosítani attól függően, hogy milyen típusú változót kell velük feltölteni. A beolvasó utasítás formája: cin >> változó. A cout (consol output) objektumon keresztül a szabványos kimenetre (például a képernyő úgynevezett konzolablakára) szánt adatot (egy kifejezés értékét) karaktersorozatra bontva küldhetjük el. A kiírás utasítása: cout << kifejezés. 27

28 1. Feladat: Osztási maradék Olvassunk be két természetes számot a billentyűzetről, határozzuk meg az osztási maradékukat, majd az eredményt írjuk ki a konzolablakba! Specifikáció A feladatban három darab, természetes számot (nem negatív egészeket) értékül vevő változó szerepel, ezek között az x és y változók kezdetben bemenő adatokat tartalmaznak, amelyek rögzített kezdő értékeit jelöljük az x -vel és y -vel. Mivel az x-nek az y-nal vett osztási maradékát (x mod y) kell kiszámolnunk, az y értéke nem lehet nulla, hiszen a nullával való osztás eredménye nincs értelmezve. A = ( x, y, z : N ) Ef = ( x=x y=y y >0 ) Uf = ( x=x y=y z = x mod y ) Absztrakt program A számítások elvégzését megoldó algoritmus a célfeltételből közvetlenül adódik. A z változónak kell új értéket adni, amely az x értékének y értékével vett osztási maradéka. z := x mod y Implementálás A programkódot a klasszikus hármas tagolással adjuk meg, amely egy jól megírt esszé (bevezetés, tárgyalás, befejezés) programozási megfelelője 1. A bemenő adatok beolvasása 2. Számolás: Az absztrakt program kódja 3. Az eredmény kiírása 28

29 amelyek implementálását a korábban ismertetett öt lépésben végezzük el. Elkészítjük a program keretét, deklaráljuk a változóit, majd kódoljuk a fenti három részt. Program kerete C++ nyelven a program keretét az a main függvény adja, amelynek blokkjában (kapcsos zárójelekkel közre zárt részén) elhelyezett utasítások végrehajtásával kezdődik a program futása. A blokk utasításai pontosvesszővel vagy csukó kapcsos zárójellel végződnek. Ezek egyfelől elkülönítik az utasításokat, másfelől a közöttük levő sorrendet is egyértelműen kijelölik. Abban a forrásállományban (legyen ennek a neve most main.cpp), ahol a main függvényt elhelyezzük, még a függvény előtt le kell írnunk két sort. Az első, az #include <iostream> a konzolablakos input/output műveletek definícióját tartalmazó iostream könyvtári csomagra hivatkozik, a második arra szolgál, hogy cin, cout azonosítókat ne kelljen std::cin, std::cout formában írni. #include <iostream> using namespace std; int main() {... return 0; 29

30 A pontozott rész helyére kerülnek majd az utasítások. A return 0 a program futását leállító utasítás. A nulla érték a minden rendben üzenetet szimbolizálja. A main függvény előtti int (integer, egész szám típusú) szócska a visszatérési érték (ami tehát most a nulla) típusát jelzi. Deklarációk C++ nyelven egy változó deklarációja a kód tetszőleges helyén elhelyezett önálló utasítás: típus változó A C++ nyelvben a természetes számok típusának nincs az egész számok típusától különböző jele. (Az előjel nélküli, unsigned, egészeket mi nem használjuk.) Több azonos típusú változó deklarációjakor a közös típusnév után vesszővel elválasztva is felsorolhatjuk a változó neveket. int x, y, z; Az egész számok (itt természetes számok) típusa megengedi az alapműveletek (+, *, -, /) használatát. Az osztás itt az úgynevezett egészosztást jelenti, azaz két egész érték hányadosa is egész lesz: annyi, ahányszor az osztó megvan az osztandóban, az osztási maradék pedig elvész. Az osztási maradékot külön művelettel számolhatjuk ki. Ennek a műveletnek a jele C++ben a %. A számokra érvényesek még az összehasonlító (kisebb, nagyobb, egyenlő, nem-egyenlő) relációk is. Absztrakt program kódolása Az absztrakt programban szereplő x mod y kifejezést a C++-ban x%y alakban kell írni; az értékadás jobboldali kifejezése tehát megengedett C++ nyelvben, így maga az értékadás is megengedett. A kódolás nem okoz különösebb gondot, ha ismerjük az értékadásnak a C++ nyelvi alakját. 30

31 A változó:=kifejezés értékadást a C++ nyelvben változó=kifejezés formában írjuk. Az értékadás-utasítások után mindig pontosvessző áll kivéve, ha az nem önálló utasításként, hanem egy kifejezés részeként jelenik meg. Az absztrakt programunk C++ nyelvű kódja tehát egyetlen értékadás utasításból áll, amit az alábbi módon írunk le. z = x % y; Bemenő adatok beolvasása A beolvasás során két problémát is meg kell oldanunk. Egyfelől el kell juttatni a program futása során a felhasználó által begépelt számokat az x és y változókba, másfelől ellenőrizni kell, hogy ezek a számok a feladat szempontjából helyesek-e. Ez utóbbi valójában három féle ellenőrzést jelent. Egyrészt vizsgálni lehetne, hogy a felhasználó tényleg számot ad-e meg, mert csak ebben az esetben kerülhet a bemenő változóinkba futási hiba nélkül érték. (Ilyen ellenőrzést most nem végzünk, de később majd megmutatjuk, hogyan lehet ezt megtenni.) Másrészt, mivel a változóink egész számokat tartalmazhatnak, de a feladat csak természetes számokról szól, vizsgálni kell, hogy a beolvasott számok nem negatívok-e. Harmadrészt pedig a feladat előfeltételét is ellenőrizni kell, azaz hogy a második szám nem nulla-e. Számot (de karaktert vagy karakterláncot is) úgy tudunk beolvasni, hogy az annak értékét leíró (billentyűzeten keletkezett) jelekből képzett adatot az <enter> billentyű lenyomásának hatására a megfelelő típusú változóba irányítjuk: cin >> x Egy beolvasó utasítás általában nem áll önmagában; megelőzi azt egy kiírás. Beolvasáskor ugyanis a program futása megszakad és vár az <enter> billentyű leütésére. Megfelelő tájékoztatás hiányába a felhasználó ilyenkor nem tudja, mit csináljon, és ezért ő is várni fog, majd arra gondol: Na, ez a 31

32 program lefagyott!. Ezért a beolvasás előtt üzenjük meg a felhasználónak például a konzolablakba történő kiírással azt, hogy mit várunk tőle. Programunkban az osztandó beolvasása a következő utasításokkal végezhető el. cout << "Kérek egy természetes számot: "; cin >> x; Az adatok beolvasása után következik az adatok ellenőrzése. Azzal tehát most nem foglalkozunk, ha a felhasználó nem egész számot adna meg; egyelőre feltételezzük, hogy ez történt. Azt viszont megvizsgáljuk, hogy az első szám nem-negatív, a második szám pedig pozitív. Az ellenőrzéseket külön-külön, közvetlenül az adott adat beolvasása után végezzük. Ez egy feltételtől függő tevékenység: ha a vizsgált szám negatív, akkor hibajelzést írunk a konzolablakba és leállítjuk a program futását (Később ennél rugalmasabb megoldással is megismerkedünk.) Ellenkező esetben tovább engedjük a program futását. Ennek leírásához egy elágazás utasítást fogunk használni. Az elágazás legegyszerűbb C++ nyelvbeli formája az if utasítás. Az if kulcsszó utáni kerek zárójelpár közé kell egy logikai értékű kifejezést írni, amelyik teljesülése esetén (amikor a kifejezés igaz értékű) hajtódik végre a kerek zárójelpár után írt utasítás, az úgynevezett if ág (más nyelvekben szokták ezt then ágnak is hívni). Ez az utasítás lehet egyszerű (ilyenkor a végét pontosvessző jelzi), de lehet összetett, több utasítás szekvenciája, amelyet egy úgynevezett utasítás blokkba (kapcsos zárójelek közé) kell zárni. Az if ágnak a végrehajtása után a vezérlés többnyire az elágazás utáni utasításra kerül (hacsak az if ág mást nem ír elő). Ha az elágazás feltétele nem teljesül (értéke hamis), akkor a vezérlés átugorja az if ágat, és az elágazás utáni utasításnál folytatódik. if(feltétel) utasítás; vagy if(feltétel) { utasítás1; 32

33 utasítás2;... A feltétel egy logikai értéket hordozó kifejezés. Ilyen kifejezés bármelyik alaptípus értékeiből képezhető, ha azokra alkalmazzuk az összehasonlítás relációkat. Ilyen az egyenlőség (==), a nem egyenlőség (!=), a kisebb (<), kisebb-egyenlő (<=), nagyobb (>), nagyobb-egyenlő (>=). cout << "Kérek egy természetes számot : "; cin >> x; if (x<0){ cout << "Negatív számot adott meg! "; return 1; cout << "Kérek egy pozitív természetes számot : "; cin >> y; if (y<=0){ cout << "Nem pozitív számot adott meg! "; return 1; Miután beolvastuk az x változó értékét, megvizsgáljuk, hogy negatív-e. Ha igen, akkor hibaüzenetet írunk a konzolablakba és leállítjuk a programot (return 1). Ez a leállítás visszaad egy hibakódot a futtató környezetnek, amit az tetszése szerint feldolgozhat. A 0-s hibakód a hibátlan lefutást jelzi (ez szerepel a program legvégén található return 0 utasításban), az ettől 33

34 eltérő bármelyik (például az 1-es) kód, valamilyen programhibára utal. Az y változó értékének ellenőrzésekor a nulla érték is hibásnak számít. Ha a programunkat egy integrált fejlesztő eszköz keretében futtatjuk, akkor ott többnyire hozzáépül a programunkhoz egy olyan funkció, amely a program befejeződése után még nyitva tartja az alkalmazás konzolablakát, így lesz időnk a legutoljára kiírt üzeneteket, eredményeket elolvasni, és az ablak csak külön felhasználói beavatkozásra tűnik el. Ha azonban ugyanezt a programot a fejlesztő környezeten kívül indítjuk el, akkor nem fog működni ez a funkció: a program befejeződésekor a konzolablak eltűnik. Ez a jelenség kivédhető, ha a programunk minden kilépési pontjánál elhelyezünk egy várakozó utasítást. Operációs rendszertől független, tehát általános, minden környezetben megfelelően működő, ugyanakkor felhasználóbarát várakozó utasítást nem könnyű találni, a C++ nyelvben sincs ilyen. Van azonban egy általános, minden környezetben működő bár nem túl elegáns megoldás a várakozás megoldására. char ch; cin >> ch; Ez a két utasítás egy karakter típusú változóba olvas be értéket (karaktert), és amíg a felhasználó a karakter begépelése után nem üt <entert>-t, addig várakozik. Nem túl kényelmes megoldás, hiszen két billentyű leütése kell hozzá, de univerzális. Eredmény kiírása Adatot, egy kifejezés értékét úgy tudunk kiírni, hogy azt a cout-ra irányítjuk. A z változóban keletkező eredményt a cout << z utasítás jeleníti meg. Az eredményt azonban mindig egy megfelelő kísérő szövegbe ágyazva kell kiírni. 34

35 Egy szöveges üzenet kiírása a konzolablak aktuális sorába: cout<<"szöveg". A cout<<endl utasítás hatására a további kiírások a konzolablak soron következő sorának elejénél folytatódnak. Az endl az std névtérben definiált speciális jel, amely kiírásával sort emelhetünk, azaz előírhatjuk, hogy a további kiírás a következő sorban folytatódjon. cout << "első sor" << endl << "második sor"; A rövidebb leírás érdekében a fenti kódrészletet az alábbi formában is írhatjuk. cout << "első sor\nmásodik sor"; A \n (újsor newline) egy speciális, úgynevezett vezérlő karaktert jelöl, amelynek hatására a további kiírások a konzolablak soron következő sorának elejénél folytatódnak. Ennek megfelelően a cout<< \n és cout<<endl utasítások hatása megegyezik. Az alábbi kódrészletben láthatjuk, hogy több részből álló kiírást hogyan lehet egyetlen kiíró utasításba befoglalni. Itt a kiírás a balról jobbra sorrend szerint történik. Megjegyezzük, hogy ezzel a technikával beolvasni is lehet egyszerre több adatot. cout << endl << x << " mod " << y << " = " << z; Tesztelés A program kipróbálásához kétféle teszteset-csoportot készíthetünk, majd az egyes esetekre jellemző minta-adatokkal kipróbáljuk a programot. Hibás működés után, ha a hiba okát felleltük és javítottuk, újra kell kezdeni a teljes tesztet, hiszen lehet, hogy a javítással elrontottunk olyan részeket, amelyek korábban már jók voltak. Fekete doboz tesztelésről akkor beszélünk, amikor a programot a programkód ismerete nélkül teszteljük, azt vizsgáljuk, hogy a feladat 35

36 szempontjából helyes-e a program. Ilyenkor a teszteseteket a feladat szempontjából lényeges vagy extrém adatok kipróbálásának céljából készítjük. Ezek között megkülönböztetjük az érvényes teszteseteket (amikor a bemenő adat kielégíti a specifikáció előfeltételét) az érvénytelen tesztesetektől (amikor azt vizsgáljuk, hogyan viselkedik a program az előfeltételt nem kielégítő adatokra). Érvényes tesztesetek: 1. Olyan számpárok, amikor az osztási maradék nulla (Pl: 24, 8 ) 2. Olyan számpárok, amikor az osztási maradék egy (Pl: 9, 4 vagy 9, 8 vagy 1, 7) 3. Olyan számpárok, amikor az osztási maradék az osztónál pont eggyel kisebb (Pl: 9, 5) 4. Olyan számpárok, amikor az osztandó kisebb az osztónál (Pl: 4, 5) 5. Olyan számpárok, amikor az osztandó nulla (Pl: 0, 7) Érvénytelen tesztesetek: 1. Olyan számpárok, amikor az osztó nulla. (Pl: 15, 0). 2. Mindkét szám nulla. 3. Olyan számpárok, amikor az osztandó negatív. (Pl: -6, 8) 4. Olyan számpárok, amikor az osztó negatív. (Pl: 6, -8) 5. Olyan számpárok, amelyek negatívak. (Pl: -6, -8) A fehér doboz tesztelés a programkód alapján készül. Mivel a programunk szerkezete most egyáltalán nem bonyolult, ennek a tesztelésnek itt nincs nagy jelentősége. Legfeljebb az adatellenőrzési részek elágazásainak tesztelésével lehetne foglalkozni. Ezeket azonban az érvénytelen tesztesetek során már kipróbáltuk. 36

37 Teljes program Végül nézzük meg teljes egészében a programkódot. Az utasítások között megjegyzéseket (kommenteket) is elhelyeztünk a könnyebb áttekinthetőség végett. Megjegyzésnek számít a // utáni sor, illetve a /* és */ által közrefogott szöveg, amelyeket a fordító figyelmen kívül hagy. Látható, hogy az utasítások végét többnyire pontosvessző jelzi. Az, hogy mi számít utasításnak, mikor lehet a pontosvesszőt elhagyni, később tisztázzuk. Egyelőre jegyezzük meg azt, hogy a deklarációk, értékadások, beolvasások és kiírások után, valamint a using namespace std és return 0 utasítások után mindig van pontosvessző. #include <iostream> using namespace std; int main() { int x, y, z; // Adatok beolvasása és ellenőrzése cout << "Kérek egy természetes számot: "; cin >> x; if (x<0){ cout << "Negatív számot adott meg! "; return 1; 37

38 cout << "Kérek egy pozitív természetes számot: "; cin >> y; if (y<=0){ cout << "Nem pozitív számot adott meg! "; return 1; // Számítás z = x % y; // Eredmény kiírása cout << endl << x << " mod " << y << " = " << z; return 0; 38

39 2. Feladat: Változó csere Cseréljük ki az egész számokat tartalmazó két változó értékeit! Olvassunk be két egész számot a billentyűzetről, végezzük el a cserét, majd az eredményt írjuk ki a konzolablakba! Specifikáció A feladatban két egész szám típusú változó szerepel, amelyek egyszerre bemenő- és eredmény-változók is. A specifikációban lényeges szerepet játszanak a változók kezdetben megadott értékei (x és y ), hiszen így tudjuk a célfeltételben leírni azt, hogy az x változó az y kezdőértékét, az y változó pedig az x kezdőértékét veszi fel. A bemenő értékekre semmilyen megszorítást nem kell tenni. A = ( x, y : Z ) Ef = ( x=x y=y ) Uf = ( x=y y=x ) Absztrakt program A számítások elvégzését megoldó algoritmus a célfeltételből adódó szimultán értékadásból áll. x, y := y, x Az x,y:=y,x szimultán értékadást úgy kell érteni, hogy egyidejűleg hajtjuk végre az x:=y és az y:=x értékadásokat. Arra kell itt majd figyelni, hogy ez a szimultán értékadás nem ekvivalens az x:=y és y:=x értékadások szekvenciájával. Implementálás 39

40 A programkód előállításánál először elkészítjük a program keretét, majd deklaráljuk a változókat, kódoljuk az absztrakt programot, amely elé a két változó kezdő értékének beolvasását végző kód, utána pedig ugyanezen változók értékét kiíró kód kerül. Program kerete #include <iostream> using namespace std; int main() {... return 0; Deklarációk int x, y; Absztrakt program kódolása Absztrakt programjainkban gyakran alkalmazunk szimultán értékadásokat. Ezek egyszerre több változó számára jelölnek ki új értékeket. A C++ nyelvben ezek közvetlen kódolására nincs lehetőség, azt előbb szét kell bontanunk egyszerű értékadások egymás után történő végrehajtására, azaz szekvenciájára. Például az x,y := 3,5 értékadást az x:=3 és y:=5 értékadások 40

41 tetszőleges sorrendje helyettesítheti. Az x,y:=y,5 értékadás (az x vegye fel y eredeti értékét, az y új értéke pedig legyen 5) felbontásánál viszont észre kell vennünk, hogy az y:=5 értékadást csak az x:=y értékadás után szabad végrehajtani, különben az x nem kapja meg y eredeti értékét, hanem csak az újat, az 5-öt. Az x:=y értékadás függ az y:=5 értékadástól, pontosabban annak helyétől. Az x,y:=y,x szimultán értékadásban az x:=y és az y:=x értékadások kölcsönösen függenek egymástól. Ezért ahhoz, hogy a szimultán értékadást egyszerű értékadások szekvenciájára felbontsuk, egy segédváltozót is be kell vezetni: z:=x; x:=y; y:=z. Ha speciálisan egész típusú változókról van szó mint most a példában akkor egy másik lehetőség is kínálkozik. Az itt felírt változatok helyességét az első kötetben tárgyalt módszerrel láthatjuk be. z := x x := y y := z x := x - y y := x+ y x := y - x Ennek megfelelően az absztrakt program kódja vagy int z; z = x; x = y; y = z; vagy x = x - y; 41

42 y = x + y; x = y - x; lesz. Az első változat a segédváltozó deklarációját is tartalmazza. Bemenő adatok beolvasása A beolvasás rész igen egyszerű. Mindössze azt kell egyértelművé tenni, hogy mikor adunk értéket az első, mikor a második változónak, hiszen az eredményt csak ennek fényében tudjuk majd értelmezni. cout << "Az első változó értéke (egész szám): "; cin >> x; cout << "Második változó értéke (egész szám): "; cin >> y; Adatellenőrzésre most nincs szükség: a feladat előfeltétele ugyanis nem tett megszorítást és feltételezzük, hogy a felhasználó (megfelelő méretű) egész számokat fog megadni. Eredmény kiírása Az eredmény megjelenítése nem kíván magyarázatot. Tesztelés cout << "Első változó új értéke: " << x << endl; cout << "Második változó új értéke: " << y << endl; Fekete doboz tesztelésnél kipróbálhatunk azonos értékeket (3,3 vagy -12,- 12), olyanokat, amikor az első szám nagyobb (67,23 vagy 12,-4 vagy -34,- 42

43 102), és amikor a második szám a nagyobb. Ezután ebben a programban külön fehérdoboz teszteseteket már nem kell generálni. Teljes program Végül nézzük meg teljes egészében a programkódot megjegyzésekkel és várakozó utasítással kiegészítve. Absztrakt program kódjaként a második változat szerepel. #include <iostream> using namespace std; int main() { int x, y; // Adatok beolvasása és ellenőrzése cout << "Első változó értéke (egész szám): "; cin >> x; cout << "Második változó értéke (egész szám): "; cin >> y; // Számítás x = x - y; y = x + y; x = y - x; // Eredmény kiírása cout << "Első változó új értéke: " << x << endl; cout << "Második változó új értéke: " 43

44 << y << endl; char ch; cin >> ch; return 0; C++ kislexikon Program kerete #include <iostream> using namespace std; int main() { return 0; Deklaráció Értékadás Beolvasás billentyűzetről Kiírás képernyőre: típus változó változó = kifejezés cin >> változó cin >> változó1 >> változó2 cout << kifejezés1 << kifejezés2 cout << endl 44

45 2. Strukturált programok Ebben a fejezetben arra szeretnénk rávilágítani, hogy hogyan lehet minimális, de jól megválasztott programnyelvi készlettel egy tetszőleges absztrakt programot kódolni. Konkrét feladatok megoldásán keresztül ismerkedünk meg a C++ nyelv alaptípusaival és vezérlési szerkezeteivel. Implementációs stratégia Az elemi programok adott programnyelvre történő leképezése többnyire igen egyszerű. Az üres (semmit nem csináló, SKIP) program kódolásához általában semmit nem kell írni; az értékadásnak is minden nyelven van megfelelője. A szimultán értékadást azonban általában nem tudjuk közvetlenül kódolni (kivétel például a Python nyelv), ezért azt előbb át kell alakítani közönséges értékadások szekvenciájára (lásd előző fejezet 2. feladat), és ha szükséges, ehhez egy vagy több segédváltozót is bevezethetünk. Ha a kódolandó programrész egy nevezetes vezérlési szerkezet, akkor az annak megfelelő nyelvi elemet kell használni. Egy szekvencia esetén külön kódoljuk a szekvencia egyes tagprogramjait, és azokat egymás után, a szekvenciában rögzített sorrendben írjuk le. Elágazásnál az egyes ágakat adó programokat kell külön-külön kódolni, és azokat az elágazás utasításba beágyazni. Ciklus esetén a ciklusmag kódját kell a ciklus-utasításban elhelyezni. d,c := n,m értékadás c d ciklus- szekvencia c<d d<c elágazás- utasítás 45

46 d := d c c := c d értékadás utasítás 2-4. ábra. Strukturált program kódolása Egy strukturált program kódolása a benne levő programszerkezetek mentén, kívülről befelé vagy belülről kifele haladva történik. A kívülről befele haladó stratégia (top-down) során az absztrakt programban megkeressük annak legkülső vezérlési szerkezetét, meghatározzuk e szerkezet komponenseit (szekvencia esetében az egymást követő részprogramokat, elágazásnál az egyes ágak programjait és a hozzájuk tartozó feltételeket, ciklusnál a ciklusmagot és a ciklusfeltételt), és leírjuk az adott szerkezetet kifejező utasítást. Ennek meghatározott pontjain kell majd a komponensek kódjait elhelyezni. Ezután a programkomponenseken megismételjük az előző eljárást. Ha egy részprogram már nem összetett (üres vagy értékadás), akkor a megfelelő elemi utasítással kódoljuk. A belülről kifele haladó stratégiánál (bottom-up) éppen az ellenkező irányba haladunk. Először az absztrakt program elemi utasításait kódoljuk, majd mindig kiválasztjuk az absztrakt programnak egy olyan vezérlési szerkezetét, amelynek részprogramjait már kódoltuk, és a nyelv megfelelő vezérlési utasításával ezeket a részprogramokat egy közös kódba fogjuk össze. Az absztrakt programok kódolása során változókat használunk, ezeket deklaráljuk, értéket adunk nekik és az értéküket felhasználjuk. Ehhez ismerni kell a használható (megengedett) adattípusokat, azokat a szabályokat, ahogyan a típusok műveleteinek segítségével kifejezéseket tudunk szerkeszteni a változókból és a típusértékekből. Ezek a kifejezések jelennek meg az értékadások jobboldalán, az elágazások és ciklusok feltételében. Nyelvi elemek 46

47 A változó:=kifejezés értékadás C-szerű nyelvekben történő kódolásával az előző fejezetben már találkoztunk. Fontos, hogy a változó = kifejezés formájú utasításban a kifejezés értékének típusa meg kell, hogy egyezzen a változó típusával. Ettől azonban bizonyos esetekben eltekinthetünk. Például egy egész érték gond nélkül értékül adható valós típusú változónak. C++ nyelven ennek a fordítottja is megtehető, de ilyenkor az értékül adott valós szám törtrésze elvész, csak az egész része adódik át az egész típusú változónak. A programozási nyelvek pontosan definiálják a típuskompatibilitási szabályokat, azaz hogy milyen eltérő típusok közötti engedjük meg az értékadást, hogyan lehet egy értéket másik típusúra konvertálni, mikor következhet be adatvesztés. Ezekkel ebben a könyvben általánosan nem foglalkozunk, csak a konkrét helyzetekre nézve alakítunk ki egy biztonságos kódolási szokást. Ennek első ajánlása az, hogy törekedjünk arra, hogy egy értékadás baloldalán levő változójának és a jobboldalán levő kifejezésének azonos legyen a típusa. Az értékadás hatására a változó a kifejezés értékét veszi fel. A C-szerű nyelvekben azonban az értékadásnak értéke is van, ez pedig éppen az értékül adott kifejezés értéke. Az értékadás kifejezés tulajdonságát használjuk ki például a változó1 = változó2 = kifejezés; utasításban is, amely mindkét változónak a kifejezés értékét adja, mert az első értékadás jobboldalán látható második értékadásnak van értéke, amely a második értékadás jobboldali kifejezésének értéke. Az, hogy egy értékadás egyben kifejezés is, számos bonyodalmat okozhat. Például egy C++ program fordításakor többnyire nem kapunk hibaüzenetet akkor, ha egy egyenlőségvizsgálatnál helytelenül nem a C++ nyelvnél használatos dupla egyenlőségjelet (==), hanem a matematikában egyébként szokásos szimpla egyenlőségjelet használjuk, ami azonban itt az értékadás jele. Gondoljunk például arra, hogy el akarjuk dönteni egy változóról, hogy az értéke egyenlőe eggyel, de az i==1 kifejezés helyett hibásan az i=1 értékadást írjuk. (Nem találkoztam még olyan programozóval, aki legalább egyszer ne követett volna el ehhez hasonló hibát.) Ilyenkor magának az értékadásnak az értéke is egy, amit a C++ nyelv igaz logikai értéknek tekint. Tehát ez az értékadás egy logikai kifejezésnek is tekinthető, amelynek az értéke igaz, miközben végrehajtásakor mellékesen a változó az egyet veszi fel új értékként. A fordítóprogram nem jelez hibát, de futáskor nem a várt működés következik 47

48 be, ráadásul az érintett változó elveszíti a korábbi értékét, hiszen felülírjuk az eggyel. Ezt a nem várt jelenséget elkerülendő célszerű, ha az az i==1 jellegű vizsgálatokban a konstanst írjuk baloldalra: 1==i, ugyanis az 1=i kifejezés esetén biztosan fogunk hibajekzést kapni. C++ nyelvvel speciális formájú értékadások is írhatók. Önálló utasításként a ++i vagy az i++ hatása egyenértékű az i = i+1 értékadáséval, a --i vagy i-- hatása pedig az i = i-1 értékadáséval. Ilyenkor érdemes a prefix változatokat (++i, --i) használni. Ha viszont egy kifejezésbe ágyazzuk ezeket a speciális értékadásokat, akkor nem mindegy, hogy melyik alakot használjuk. Az i++ egy kifejezésben az i kezdeti értékét mutatja, és csak utána fogja megnövelni eggyel. A ++i előbb végzi el az i növelését, és a kifejezés ezt megnövelt érékét képviseli. (Hasonló a helyzet a --i vagy i-- értékadásokkal.) Érdekes lehet tudni, hogy az olyan értékadásokat, mint i=i+a, i=i-a, i =i*a, stb. rövidebb alakban is lehet C++ nyelven írni: i+=a, i-=a, i*=a, stb. Megjegyezzük végül azt is, hogy egy változó deklarációja összevonható azok első (kezdeti) értékadásával. A típus változó = érték utasítás a változó definíciójával egyidejűleg kezdőértéket is ad a változónak. C++ nyelven a változók deklarálása is egy elemi utasítás. Ennek során foglalódik le a változóban tárolt érték számára a megfelelő memória terület. Ennek mérete a változó típusától függ. Tekintsük most át a leggyakrabban használt alaptípusokat: az egész szám, a valós szám, a logikai, a karakter és a karakterlánc típust. Az egész szám típusának egyik formájával, az int-tel már találkoztunk. Ezt használjuk a természetes számok típusának kódolására is. Az egész számokat többnyire úgynevezett kettes komplemens kódban ábrázolják a memóriában. Ez a kódolás a kettes számrendszerbeli (bináris) ábrázoláson túl a negatív számok jelölésének sajátos technikáját jelenti. A programozási nyelvek (C++ is) több fajta egész típust is bevezet. Ezek abban különböznek, hogy eltérő méretű memória területet jelölnek ki a változó számára, így különbözik az általuk ábrázolható legnagyobb és legkisebb egész 48

49 szám. Olyan egész szám típus nincs, amelyikkel az összes egész számot ábrázolni lehet, hiszen a memória végessége miatt minden esetben ábrázolási korlátokba ütközünk. Az, hogy legfeljebb és legalább mekkora egész számot lehet tárolni egy egész típusú változóban, például az int típus esetén, az fordítóprogram függő. Az egész számokra, illetve az egész típusú változókra alkalmazhatjuk az alapműveleteket (+, -, *, /, %), az összehasonlító relációkat és néhány beépített függvényt. Ne felejtsük el, hogy két egész szám osztásának eredménye is egész szám lesz. A műveletek nem minden esetben adnak helyes eredményt: ha az eredmény nem ábrázolható a választott egész típus által kijelölt memória területen, akkor túlcsordulás, és ebből fakadó adatvesztés jön létre. 49

50 Egész számok kettes komplemens kódja Az egész számok általánosan elterjedt kódolása. A számokat s biten ábrázoljuk úgy, hogy a szám abszolút értékének bináris alakja nullával kezdődjön. Ezért az ábrázolható számok -2 s-1 és 2 s-1 1 közé kell, hogy essenek, ellenkező esetben túlcsordulásról beszélünk. A pozitív egész számot annak bináris alakjával kódoljuk (az első bit 0 lesz, ami jelzi, hogy a szám pozitív), a negatív egész szám esetén a szám abszolút értékének bináris alakját bitenként invertáljuk (0-ból 1-et, 1-ből 0-át csinálunk) és az így kapott s jegyű számhoz hozzáadunk egyet. (Az első bit 1 lesz, ami jelzi, hogy a szám negatív.) 1. Példa. Adjuk meg a +12 egész szám kettes komplemens kódját 4 bájton! 12 (10) = (2) 2. Példa. Adjuk meg a -12 egész szám kettes komplemens kódját! i) 12 (10) = (2) ii) Vegyük a bináris alak komplemensét (invertált alakját)! iii) Adjunk hozzá binárisan 1-et! A valós számok típusára is több lehetőség kínálkozik a C++ nyelvben, de tisztában kell lenni azzal, hogy ezek mindegyike csak véges sok valós szám ábrázolására képes. Mindig lesznek olyan valós számok, amelyek vagy olyan nagy abszolútértékűek, hogy egyáltalán nem jeleníthetőek meg (túlcsordulás), vagy olyanok, amelyek helyett csak egy hozzájuk közel eső másik valós számot tudunk ábrázolni (kerekítési hiba). Ez utóbbi speciális esete az alulcsordulás, amikor nagyon kicsi számok helyett a nulla kerül ábrázolásra. (Ez akkor jelent különösen problémát, ha egy ilyen számmal például osztanunk kell.) A választható valós típusok a float, double és a long double, amelyek a valós számokat lebegőpontosan ábrázolják. Mi általában a double típust fogjuk használni. A típushoz tartozó értékeinek leírásában szerepel vagy a tizedes pont, vagy a lebegőpontos alak jelzésére utaló e betű: 50

51 e e-15 1e10 Valós számok lebegőpontos számábrázolása A valós számok IEEE 754 szabvány szerinti (4 illetve 8 bájtos) lebegőpontos ábrázolásánál a számokat ((-2)*b+1)*m*2 k alakban írjuk fel, ahol b az előjelbit (0 - pozitív, 1 - negatív), az m a mantissza (1 m<2), a k pedig a karakterisztika. Maga a kód a valós szám előjelbitjéből (1 bit), utána s biten (az s 8 illetve 11 bit) a karakterisztikának 2 s-1 1 eltolású többletes kódjából (ekkor -2 s-1 +1 k 2 s-1 ), végül a mantissza bináris alakjának törtrészéből (23 illetve 52 biten) áll. A lebegőpontos ábrázolás fő előnye, hogy egy szám kerekítési hibája függ a szám nagyságrendjétől. Kicsi számok esetén az ábrázolás csak kis hibát enged meg, nagy számok esetén a hiba nagyságrendje is nagyobb. (A többletes kód azt jelenti, hogy egy egész számhoz, mielőtt binárisan ábrázoljuk, hozzáadunk egy rögzített értéket. Ennek az eltolásnak a mértékét úgy állapítjuk meg, hogy az ábrázolni kívánt számok az eltolás után ne legyenek negatívak. Például ha s biten kívánjuk a számainkat ábrázolni, akkor az eltolás lehet a 2 s-1. Ekkor az jelenti a nullát és az ábrázolható számok -2 s-1 és 2 s-1 1 közé esnek. Az első bit ekkor is a szám előjelét jelzi, de 1 a pozitív és 0 a negatív jele. Ha az eltolásnak a 2 s-1 1-et választjuk, akkor a nulla kódja lesz, az ábrázolható számok -2 s-1 +1 és 2 s-1 közé esnek. Elsősorban számok nagyság szerinti összehasonlításához, számok összegének és különbségének kiszámolásához alkalmas ez az ábrázolás. A lebegőpontos ábrázolás karakterisztikájával szemben éppen ezek az elvárásaink.) Példa: Adjuk meg a valós szám lebegőpontos kódját biten! Negatív szám, ezért az előjel bit 1 lesz: b = 1 A szám abszolút értékének bináris alakja: (10) = (2) Normalizáljuk az így kapott számot 1 és 2 közé: (2) = (2) *2 3 Ábrázoljuk a karakterisztikát többletes kódban 11 biten: (10) = (2) A kód:

52 A double (float, long double) típusú értékekre és változókra megengedettek az alapműveletek, ezeken kívül a cmath csomagban számos olyan matematikai függvényt találhatunk, amelyet valós számokra értelmezhetünk. Ilyen például az sqrt() függvény, amellyel négyzetgyököt vonhatunk egy nem-negatív valós számból. A karakter típus jele C++ nyelven a char. Egy karakter-változó karaktereket vehet fel értékül. A típus értékeit aposztrófok közé írva tudjuk a kódban közvetlenül leírni. Karaktereket össze lehet hasonlítani az egyenlő (==) és a nem-egyenlő (!=) operátorokkal. Érvényesek a kisebb és nagyobb relációs jelek is, de ezek eredménye a vizsgált karakterek kódjától függ. Karakterek ábrázolása Egy adott karakterkészlet elemeit, karaktereit megsorszámozunk 0-val kezdődően, és ezeket a sorszámokat tároljuk binárisan kódolva a megfelelő karakter helyett. Az egyik legegyszerűbb az ASCII karakterkészlet, amelyik 256 különböző karaktert tartalmaz. A 0 és 255 közötti sorszámok bináris kódja egy bájton ábrázolható. Az UTF-8 változó hosszú kódolással ábrázolja a karaktereket (magába foglalja és 1 bájton ábrázolja az ASCII karaktereket, de például az ékezetes betűk sorszámai 2 bájton kódolódnak.) A karakterláncok típusa a C++ nyelven a string. Ellentétben a többi alaptípussal, ennek használatához szükség van az #include <string> sorra, és a using namespace std nélkül pedig csak std::string alakban használhatjuk. A típus értékeit idézőjelek közé írt karakterlánccal írhatjuk le. A C++ nyelv a C nyelvből fejlődött ki, és ezért számos, a C nyelvben is használható elemet is tartalmaz. A C nyelvben a karakterlánc egy \0 értékkel lezárt karaktersorozat. A C++ nyelvben e helyett jelent meg a string típus, de továbbra is használhatók még C stílusú karakterláncok is. Ennek használata akkor indokolt, amikor olyan műveletekre van szükség, amelyeket csak a C stílusú karakterláncokra definiál a nyelv. Ilyenkor a C++ stílusú karakterláncot C stílusú karakterlánccá kell átalakítani. Erre szolgál a c_str() függvény: ha str egy string típusú változó, akkor az 52

53 str.c_str() kifejezés az str-ben található karakterlánc C stílusú megfelelője lesz. Karakterlánc ábrázolása A karakterlánc egy változtatható hosszúságú karaktersorozat. A karakterek kódját sorban egymás után tároljuk el. A sorozat hosszát vagy egy külön számláló vagy a lánc végén utolsó utáni karakterként elhelyezett speciális karakter ( \0 ) jelzi. A \ karakternek egy karakterláncban általában speciális jelentése van: az azt követő karakterrel együtt többnyire a karakterlánc megjelenítésével kapcsolatos tevékenységet vezérli. Ilyen például a \n, amelyik a karakterlánc kiírásakor nem látszik, de a kiírásnál sortörést eredményez. Vagy a \t, amely után a kiírás egy tabulátornyival jobbra folytatódik. Ha viszont kifejezetten a \-t tartalmazó sztringet akarjuk leírni, akkor azt meg kell duplázni: "c:\\program Files". Egy string típusú str változóra nagyon sok művelet alkalmazható. Lekérdezhetjük a hosszát: str.size() vagy str.length(); lekérdezhetjük vagy megváltoztathatjuk az i-edik karakterét: str[i]; a + jel segítségével összefűzhetünk két karakterláncot. Lehetőség van a karakterláncok lexikografikus összehasonlítására is. Egy karakterláncnak ki lehet hasítani egy darabját is: str.substr(honnan, mennyit). A find() függvénycsalád (sok változata van) egy sztringben keres karaktert vagy részsztringet, annak sztringbeli pozícióját adja vissza, ha nem talál, akkor a string::npos extremális értéket. Lehet a sztring adott pozíciójától kezdődően keresni az első vagy az utolsó előfordulást. A sztringet megváltoztató függvények között találjuk a karaktert vagy rész-sztringet adott pozícióra beszúró (insert), adott pozícióról törlő (erase), adott pozíción helyettesítő (replace) műveleteket. A logikai típus jele C++ nyelvben a bool. A típus értékei a false (hamis) és a true (igaz). A típus műveletei a logikai és (jele: &&), a logikai vagy (jele: ) és a tagadás (jele:!). Logikai értékű kifejezéshez jutunk, ha tetszőleges típusú kifejezések között alkalmazzuk a relációs jeleket (==, <, 53

54 <=, >, >=). Ilyen kifejezésekből és logikai változókból összetett logikai kifejezéseket is szerkeszthetünk. A logikai műveleti jelek eltérő prioritásúak: a tagadás a legerősebb, a vagy a leggyengébb művelet. Például az u&&!v w kifejezés egyenértékű az (u&&(!v)) w kifejezéssel. Ha nem vagyunk biztosak a prioritási szabályokban, akkor inkább zárójelekkel tegyük magunk számára egyértelművé a kifejezéseinket. Logikai érték ábrázolása Habár a logikai értékek (igaz vagy hamis) tárolására egyetlen bit is elég lenne, de mivel a legkisebb címezhető egység a memóriában a bájt, ezért legalább egy bájtot foglalnak el. A nulla értékű (csupa nulla bit) bájt a hamis értéket, minden más az igaz értéket reprezentálja. A programozási nyelvek az utasítások végrehajtási sorrendjét (szekvenciáját) az utasítások egymás utáni elhelyezésével jelölik ki. Az utasítások végrehajtása ennek a sorrendnek megfelelően, elejétől a végéig, egymás után szekvenciában történik. Ez egyben azt is jelenti, hogy a szekvencia vezérlési szerkezetet az őt alkotó programok kódjainak egymás után írásával tudjuk kifejezni. Több utasítás szekvenciája befoglalható egy úgynevezett utasításblokkba, amelyet a fordító egyetlen bár összetett utasításként kezel. Vannak nyelvek, ahol a szekvencia minden tagját új sorban kezdve kell leírni, de a C++ nyelven ez nem szükséges, hiszen egyértelműen látszik egy utasításnak a vége, ami egy pontosvessző vagy egy csukó kapcsos zárójel. Általánosan elfogadott szokás azonban, hogy a szekvencia minden egyes részét külön sorban kódoljuk. Több utasítás szekvenciáját egyetlen összetett utasításba zárhatunk, ha azt utasításblokként egy nyitó és egy csukó kapcsos zárójel közé helyezzük. Ezt a blokkot nem követi pontosvessző, de a blokkbeli utasításokat pontosvessző zárja le. C++ nyelven olyan speciális utasítások is írhatóak, amelyek ugyancsak szekvenciát kódolnak: ilyen például a már említett változó1 = változó2 = kifejezés utasítás, amely először a változó2-nek adja értékül a kifejezés értékét, majd a 54

55 változó1-nek. Az ilyen rövidítésekkel azonban óvatosan bánjunk, mert már egy egyszerűbb összetétel értelmezése sem egyértelmű (pl. i = j = i++). A programozási nyelvek általában rendelkeznek az úgynevezett if-then elágazás utasítással, (C++ nyelven: if(feltétel) ág ), amely az alábbi absztrakt program megfelelője: feltétel ág SKIP Az elágazás utasításnak szokott lenni egy különben (else) ága is, amely akkor kerül végrehajtásra, ha az if feltétele nem teljesül. Ilyenkor a vezérlés az if ágat átugorva az else ágra kerül. Ha nem írunk else ágat, az olyan mintha az else ág az üres (semmit nem csináló) program lenne. C++ nyelven az ágak állhatnak egyetlen egyszerű (pontosvesszővel lezárt) utasításból, vagy több utasítást tartalmazó utasításblokkból. Összetett ágak esetén a C++ nyelven többféle írásmódot is szoktak alkalmazni: if(feltétel) if(feltétel){ if(feltétel){ { ág_1 ág_1 ág_1 else{ else{ ág_2 else ág_2 { ág_2 Amikor az egyes programágak egyetlen utasításból állnak, akkor nem szükséges az utasításblokk használata. Azt javasoljuk azonban, hogy egyetlen utasításból álló ágat is zárjunk utasításblokkba. Ha ezt mégsem tennénk, 55

56 akkor ilyenkor az utasításokat az if illetve else kulcsszóval azonos sorba írjuk. if(feltétel) utasítás_1; else utasítás_2; Az if-then-else utasítás tehát lehetővé teszi az olyan struktogrammok kódolását is, ahol egyik ág sem üres program. Egy általános elágazásnak azonban kettőnél több ága is lehet, és minden ágához egyedi feltétel tartozik, azaz egy ág nem akkor hajtódik végre, ha egy másik feltétele nem teljesül. Viszonylag kevés programozási nyelv rendelkezik az ilyen valódi többágú elágazást kódoló utasítással. feltétel 1 feltétel 2... feltétel n ág 1 ág 2... ág n Itt jegyezzük meg, hogy az absztrakt többágú elágazás nemdeterminisztikus (ha több feltétel is igaz, akkor nem eldöntött, hogy melyik programág hajtódik végre), és abortál, ha egyik feltétel sem teljesül. Éppen ezért nem teljesen egyenértékű a fenti struktogramm és annak átalakításával kapott alábbi változat, amely egymásba ágyazott if-then-else elágazásokkal már egyszerűen kódolható: feltétel 1 feltétel 2 feltétel n ág 1 ág 2 ág n SKIP Az átalakított változat minden esetben az első olyan ágat hajtja végre, amelyik feltétele teljesül, és üres programként működik, ha egyik feltétel 56

57 sem igaz. A két változat csak akkor lesz azonos hatású, ha az elágazás feltételei teljes és páronként diszjunkt rendszert alkotnak. Ennek hiányában is igaz azonban az, hogy ha egy feladat megoldásához egy helyesen működő sokágú elágazást terveztünk, akkor annak a fent bemutatott átalakított változata is helyesen fog működni. C++ nyelven a sokágú elágazást egymásba ágyazott if-else-if elágazásokkal kódolhatjuk: if(feltétel_1){ ág_1 else if(feltétel_2){ else{ ág_2 ág_n+1 A struktogrammokban az úgynevezett elöl tesztelő ciklust használjuk. Egy elöl tesztelő ciklus mindannyiszor újra és újra végrehajtja a ciklusmagot, valahányszor a megadott feltétel (ciklusfeltétel) teljesül. feltétel mag Az absztrakt program (elöl tesztelős) ciklusát C++-ban a while( ){ utasítással kódoljuk, ahol a while kulcsszó utáni kerek zárójelpár tartalmazza a ciklusfeltételt, az azt követő kapcsos zárójelpár pedig a 57

58 ciklusmagot. (A kapcsos zárójelpár ugyan elhagyható, ha a ciklusmag egyetlen utasításból áll, de ezt az írásmódot nem javasoljuk. Ha mégis megtennénk, akkor ebben az esetben a ciklusutasítást írjuk egy sorba.) Általában az alábbi írásmódok valamelyikét használjuk: while(feltétel){ mag { while(feltétel) mag Azoknál a programozási nyelveknél, ahol a változók deklarálásának a helye kötött (például a Pascal nyelv), minden változót a program kijelölt helyén kell deklarálnunk. Más nyelvek esetében a változó deklarációkat a végrehajtható utasítások között lehet elhelyezni (például a C++ nyelvben). Mindkét esetben figyelembe kell venni azonban a változók egy speciális tulajdonságát, a láthatóságot. Egy változó láthatóságán a programszöveg azon részét értjük, ahol a változóra hivatkozni lehet (azaz ahol leszabad írni a változó nevét). Ez ugyanis nem feltétlenül lesz a teljes programszöveg. A programszöveg gyakran több, akár egymásba ágyazható egységre, blokkra bontható. A C++ nyelven például minden kapcsos zárójelek közé írt kód önálló blokkot alkot, de önálló blokk egy elágazás- illetve a ciklusutasítás is. A blokkokat egymásba ágyazhatjuk. Maga a main függvény is egy blokk, jelenlegi programjainkban ez a legkülső. Egy blokkban deklarált változót csak az adott blokkban a deklarációt követő részben használhatunk, csak ott látható. Éppen ezért körültekintően kell eljárni a változó deklaráció helyének megválasztásakor. Például az úgynevezett segédváltozókat elég csak azokban a blokkokban, ahol ténylegesen használjuk őket. Ha a programblokkok egymásba ágyazódnak, akkor a külső blokkban deklarált változó a belső blokkban is látható. Ezt a beágyazott programblokkra nézve globális változónak szokták nevezni. A belső blokkban deklarált változó viszont csak ott lesz látható, a külsőben nem, ez tehát egy lokális változó. Itt most egy relatív értelemben vett globalitási fogalomról beszélünk. (Ismeretes ezeknek a fogalmaknak egy szigorúbb értelmezése is, amikor a programban mindenhol látható változót nevezik 58

59 globális változónak, a többit lokálisnak. Ilyenkor abszolút értelemben vett globalitásról beszélünk.) Végezetül fontos felhívni a figyelmet arra, hogy a programkódot megfelelő eszközökkel C++ nyelven tabulálással (behúzással), kommentekkel (megjegyzésekkel) és üres sorokkal érdemes úgy tördelni, hogy ezzel szemléletessé tegyük azt, hogy egy programegység milyen mélyen ágyazódik egy másikba, ennél fogva könnyen azonosíthatjuk egy változó láthatósági körét, azaz a változó deklarációját tartalmazó programegységet. Láthatóság: A programszöveg azon része, ahol a változóra hivatkozni (a változót használni) lehet. Élettartam: A program futásának azon időtartama, amikor a változó lefoglalt memóriaterülettel rendelkezik, és ott értéket tárol ábra. Változó láthatósága és élettartama A deklaráció helye nemcsak a láthatóságot befolyásolja, hanem a változó élettartamát is. Egy változó élettartamán a program futása alatti azon időszakot értjük, amikor a változó helyet foglal a memóriában. Egy változónak csak azt követően lehet értéket adni, hogy megtörtént a változó helyfoglalása. Azokban az egyszerű programokban, amelyekkel ebben a részben találkozunk, a bemenő- vagy eredményváltozóknak általában a program teljes futása alatt élni kell, ezért a program fő egységében (C++ nyelvben ez a main függvény) kell deklarálni, hiszen ennek az egységnek a végrehajtásával kezdődik és fejeződik be a program futása, így az itt deklarált változók élettartama a teljes végrehajtásra kiterjed. Ezzel szemben a segédváltozókra csak a program működésének egy bizonyos szakaszában van szükségünk. Ilyenkor elég őket abban a program egységben deklarálni, amelynek végrehajtásakor elég ezeknek a változóknak megszületni (helyet foglalni a memóriában), hogy használjuk őket, majd megszűnhetnek (felszabadulhat az általuk foglalt hely). Ezek a változók később akár újraszülethetnek, de 59

60 ilyenkor nem emlékeznek a korábbi értékükre, hiszen nem ugyanaz a memória szelet foglalódik le számukra. A C++ nyelv megengedi main függvényen kívül történő változó deklarációkat is. Ezek a változók globálisak az adott forrásállományban a deklarációjukat követő minden programegységre nézve, így a main függvényre nézve is. Ebben a könyvben azonban nem fogunk ilyen változó deklarációkat alkalmazni. 60

61 3. Feladat: Másodfokú egyenlet Oldjunk meg a valós számok halmazán egy másodfokú egyenletet! Specifikáció Az ax 2 +bx+c=0 másodfokú egyenlet kitűzéséhez három darab, valós együtthatót (a,b,c) kell megadni. A megoldás eredménye igen változatos lehet. Ha tényleg másodfokú az egyenlet, azaz a 0, akkor az egyenlet diszkriminánsától függően három féle válasz lehetséges: két különböző valós gyök van, egy közös valós gyök van, nincs valós gyök. Ha az egyenlet elsőfokú (a=0), akkor is három féle választ kaphatunk: ellentmondás (nincs gyök), azonosság (minden valós szám megoldás), vagy egyetlen megoldás van. A válasz tehát egyrészt egy szöveg (karakterlánc) lesz, másrészt esetlegesen egy vagy két gyök (valós szám). A = ( a, b, c, x 1, x 2 : R, válasz : String ) Ef = ( a=a b=b c=c ) Uf = ( a=a b=b c=c (a 0 ( b 2 +4ac < 0 (válasz= Nincs valós gyök ) ) ( b 2 +4ac = 0 (válasz= Közös valós gyök b x1 x2 ) ) 2a ( b 2 +4ac > 0 (válasz = Két valós gyök x 1 b 2 b 2a 4ac 2 b b 4ac x2 2a ) ) ) c ( a=0 ( b 0 (válasz= Elsőfokú gyök x 1 ) ) b ( b=0 c=0 (válasz= Azonosság ) ) ( b=0 c 0 (válasz= Ellentmondás ) ) ) 61

62 Absztrakt program Az utófeltétel első pillantásra talán bonyolultnak tűnik, de aztán felfedezhető, hogy ebben egy kétágú elágazás van elrejtve, amelynek mindkét ága egy-egy háromágú elágazásból áll. Ennek megfelelően könnyen felírható az absztrakt megoldás. A megoldásban megjelenik egy új, a feladatban nem szereplő valós szám típusú segédváltozó (d) is, amely a diszkrimináns ideiglenes tárolására szolgál. Vegyük észre, hogy ezt csak az algoritmus elágazásának egyik ága használja. a 0 2 d : b 4ac b 0 b=0 c=0 b=0 c 0 d<0 d=0 d>0 válasz:= Nincs gyök válasz:= Közös gyök válasz:= Két gyök válasz:= E lsőfo-kú gyök válasz:= Azonosság válasz:= E llentmondás x 1,x 2 := a b 2 x 1,x 2 := b 2a d x 1 := b c Implementálás A programkód a klasszikus három részre tagolódik. Először a bemenő adatok beolvasását végezzük el, utána az absztrakt program kódja következik, végére az eredmény kiírása marad. Program kerete 62

63 A program kerete a korábban megismert forma lesz, amelynek ez elejére a szabványos input-output műveleteket támogató iostream csomag mellett matematikai függvényeket definiáló cmath csomag használatát is kijelöltük. Ez utóbbira a gyökvonás használata miatt lesz szükségünk. 63

64 #include <iostream> #include <cmath> using namespace std; int main() {... return 0; Deklarációk Az állapottér öt valós szám típusú változót és egy karakterlánc típusú változót tartalmazott. Bevezetünk még két logikai változót is (ez egy tipikusan implementációs döntés) a gyökök számának jelzésére. Ezeket a kiírásnál fogjuk használni. (A gyökök számának jelzésére használhattunk volna egy egész típusú változót is, de akkor nem tudtuk volna bemutatni a logikai változók használatát.) A logikai változóknak a deklarálásuknál adunk kezdő értéket. double a, b, c, x1, x2; string valasz; bool egy = false, ketto = false; Absztrakt program kódolása A kódolandó absztrakt program több, egymásba ágyazott elágazás lesz, de tartalmaz több szekvenciát is. Alapul véve a tervezésnél felírt struktogrammot egy kétágú elágazást kell készítenünk, amelynek az első ága 64

65 egy értékadásnak és egy háromágú elágazásnak a szekvenciája, a második ága pedig egy újabb háromágú elágazást tartalmaz. A d segédváltozót ott deklaráljuk, ahol szükség van rá. Hatóköre, azaz a láthatósága a külső elágazás if ágának végéig tart. Ügyeljünk arra, mikor kell értékadást (=) és mikor egyenlőséget (==) használni. Érdemes megfigyelni a programkód vízszintes és függőleges tagolását: a tabulátorjelek, üres sorok és kommentek használatát. 65

66 if (a!= 0) { d = b*b-4*a*c; // diszkrimináns kiszámolása if (d<0){ valasz = " nem rendelkezik valós gyökkel!"; else if (0 == d){ valasz = " közös valós gyökei: "; egy = true; x1 = x2 = -b/(2*a); else if (d > 0){ valasz = " két valós gyöke: "; ketto = true; x1 = (-b+sqrt(d))/(2*a); x2 = (-b-sqrt(d))/(2*a); else if (0 == a){ if (b!= 0){ valasz = " elsőfokú gyöke: "; egy = true; x1 = -c/b; else if (0 == b && 0 == c){ valasz = " azonosság!"; 66

67 else if (0 == b && 0!= c){ valasz = " ellentmondás!"; Megjegyezzük, hogy az egy és ketto változók egyszerre nem lehetnek igazak. Bemenő adatok beolvasása Az adatok beolvasása nem szorul különösebb magyarázatra. cout << "Másodfokú egyenlet megoldása!\n"; cout << "Az egyenlet ax^2+bx+c=0 alakú.\n"; cout << "Adja meg az egyenlet együtthatóit!\n"; cout << "a = "; cin >> a; cout << "b = "; cin >> b; cout << "c = "; cin >> c; A beolvasott értékeket nem kell ellenőrizni, ha feltételezzük, hogy a felhasználó tényleg számokat ad meg. Később találkozunk majd olyan megoldásokkal is, amikor azt is ellenőrizni fogjuk, hogy tényleg számot írt-e be a felhasználó. Eredmény megjelenítése Kiírjuk a válasz-t és a gyököket. Ez utóbbiakat egy háromágú elágazással annak megfelelően, hogy egyetlen gyök van, két gyök van illetve nincs gyök 67

68 (ilyenkor semmit nem kell írni, ezért a harmadik ág üres, amely meg sem jelenik a kódban). Tesztelés cout << "\naz egyenlet " << valasz << endl; if(egy){ cout << "x = " << x1; else if(ketto) { cout << "x1 = " << x1 << endl; cout << "x2 = " << x2 << endl; A program kipróbálásához most is kétféle teszteset-csoportot készítünk. A feladat szempontjából (fekete doboz tesztelés) vett érvényes teszteseteket ennél a feladatnál értelemszerűen a hat alapesetet modellező adatok alkotják, valamint a határesetként felfogható számokkal megadott bemenő adatok, mint a nulla és az egy. Érvénytelen teszteset nincs, mert továbbra sem foglalkozunk annak kezelésével, hogy mi történjen a beolvasásnál, ha számot várunk, de a felhasználó egy számként nem értelmezhető adatot ad meg. 1. (0.0,0.0,0.0) Válasz: azonosság. 2. (0.0,0.0,5.2) Válasz: ellentmondás. 3. (0.0,1.0,0.0) Válasz: elsőfokú gyöke: (0.0,1.0,-5.2) Válasz: elsőfokú gyöke: (0.0,3.0,-5.2) Válasz: elsőfokú gyöke: (1.0,1.0,1.0) Válasz: nincs valós gyök. 7. (1.0,0.0,1.0) Válasz: nics valós gyök. 8. (2.0,-5.0,6.0) Válasz: nics valós gyök. 68

69 9. (1.0,1.0,0.0) Válasz: egy valós gyök: 10. (6.0,-12.0,6.0) Válasz: egy valós gyök: (1.0,-5.0,6.0) Válasz: két valós gyök: 2.0 és (1.0,5.0,6.0) Válasz: két valós gyök: -2.0 és (1.0,-1.0,6.0) Válasz: két valós gyök: 3.0 és (1.0,1.0,-6.0) Válasz: két valós gyök: -3.0 és (3.0,-4.0,1.0) Válasz: két valós gyök: 1.0 és 0.3 Nem vizsgáljuk a kerekítésekből illetve túlcsordulásokból származó hibákat, hiszen semmilyen kódrészt nem építetünk a programba az ilyen jelenségek kivédésére. A fehér doboz tesztesetek megegyeznek a fekete doboz tesztesetekkel, hiszen azok biztosítják, hogy a programkód minden utasítása legalább egyszer végrehajtódon. Teljes program #include <iostream> #include <string> #include <cmath> using namespace std; int main() { double a, b, c; double x1, x2; 69

70 string valasz; double d; // diszkriminánst tároló változó bool egy = false, ketto = false; //gyökök száma // Beolvasás cout << "Másodfokú egyenlet megoldása!\n"; cout << "Az egyenlet ax^2+bx+c=0 alakú.\n"; cout << "Adja meg az egyenlet együtthatóit!\n"; cout << "a = "; cin >> a; cout << "b = "; cin >> b; cout << "c = "; cin >> c; // Számolás if (a!= 0) { d = b*b-4*a*c; // diszkrimináns kiszámolása if (d<0){ valasz = " nem rendelkezik valós gyökkel!"; else if (0 == d){ valasz = " közös valós gyökei: "; egy = true; x1 = x2 = -b/(2*a); 70

71 else if (d > 0){ valasz = " két valós gyöke: "; ketto = true; x1 = (-b+sqrt(d))/(2*a); x2 = (-b-sqrt(d))/(2*a); else if (0 == a){ if (b!= 0){ valasz = " elsőfokú gyöke: "; egy = true; x1 = -c/b; else if (0 == b && 0 == c){ valasz = " azonosság!"; else if (0 == b && 0!= c){ valasz = " ellentmondás!"; // Kiírás cout << "\naz egyenlet " << valasz << endl; if(egy){ // csak egy gyök van 71

72 cout << "x = " << x1; else if(ketto) { // két különböző gyök cout << "x1 = " << x1 << endl; cout << "x2 = " << x2 << endl; return 0; 72

73 4. Feladat: Legnagyobb közös osztó Olvassunk be két pozitív egész számot billentyűzetről, határozzuk meg a legnagyobb közös osztójukat, majd az eredményt írjuk ki a konzolablakba! Specifikáció A feladatunkban három darab, egész számokat értékül felvevő változó szerepel, ezek között az m és n változók a bemenő változók. Mindkét bemenő változó kezdetben pozitív értéket kell, hogy tartalmazzon. A = ( n, m, d : Z ) Ef = ( n=n m=m n >0 m >0 ) Uf = ( d = lnko(n,m ) ) A célfeltételben szereplő lnko() függvény két egész szám legnagyobb közös osztóját állítja elő. Természetesen a megoldó programban ezt a függvényt közvetlenül nem használhatjuk, mert az egész számok típusa nem rendelkezik ilyen művelettel. Absztrakt program A számítások elvégzésére olyan algoritmust választottunk, amelyben mindenféle vezérlési szerkezettel találkozhatunk, így az implementációja jó mintát ad más kódolási feladatokhoz. Ennek az algoritmusnak a helyessége az első kötet 3. fejezetében leírtak szerint ellenőrizhető. Vegyük észre, hogy a program a feladat változóin kívül egy segédváltozót (c : Z) is használ. d,c := n,m c d c<d d := d c d<c c := c d 73

74 Implementálás A program implementálását most is öt lépésben végezzük el. Program kerete A program kerete a korábban megismert forma lesz. #include <iostream> using namespace std; int main() {... return 0; Deklarációk A programban használt változók int típusúak. int m, n, d, c; Absztrakt program kódolása Az absztrakt program legkülső szerkezete szekvencia: a d,c := n,m szimultán értékadásnak és az ezt követő ciklusnak a szekvenciája. A szimultán értékadást, amely két egymástól független értékadásból áll, ezért a d = n; c = m; szekvenciával kiválthatjuk. Ezt a két értékadást azonban a kódban egy sorba írjuk, ezzel kifejezve azt, hogy egymáshoz vett sorrendjük nem lényeges. 74

75 A szimultán értékadást szekvenciában követi egy ciklus. A ciklus feltétel kódját (c!=d) gömbölyű zárójelek között a while kulcsszó után írjuk, majd egy kapcsos zárójelpárt, amely a ciklusmag helyét jelzi. A ciklusmag egy kétágú elágazás, ágai értékadások, a kódja egy if-elseif konstrukció, ahol az if kulcsszót a gömbölyű zárójelpár közé zárt feltétel követi, amelyet a megfelelő programág (értékadás) kódja követ kapcsos zárójelpár között. d = n; c = m; while(c!= d){ if (c<d){ d = d-c; else if(c>d){ c = c-d; Az absztrakt algoritmus kódjában egyáltalán nem kellett volna kapcsos zárójeleket használni, de így egyrészt jobban kihangsúlyozzuk a programszerkezetet, másrészt, ha később mégis ki kellene egészíteni újabb utasítással a ciklusmagot vagy valamelyik ágat, akkor esetleg elfelejtenénk az utasításblokk utólagos beillesztését, és ez hibás kódhoz vezetne. Bemenő adatok beolvasása és ellenőrzése Az adatok beolvasása megfelelő magyarázó szövegek kiírásával jár együtt. A kódrész végén kerül sor a beolvasott adatok közös ellenőrzésére. A feladat szerint ezek csak pozitív egész számok lehetnek. cout << "Legnagyobb közös osztó számítás" << endl; cout << "Kérem az első számot: "; cin >> m; 75

76 cout << "Kérem a második számot: "; cin >> n; if(!(m>0 && n>0)){ cout << "Hiba! Természetes számokkal dolgozok! "; return 1; Eredmény kiírása cout << endl << "LNKO = " << d << endl; 76

77 Tesztelés A feladat szempontjából készített fekete doboz tesztesetek: Érvényes tesztesetek: 1. Két nem relatív prím, összetett szám (Pl: 24, 18) 2. Két ugyanazon prímtényezővel rendelkező összetett szám (Pl: 9, 27) 3. Két relatív prím (Pl: 9, 16 vagy 6, 35) 4. Azonos számok (Pl: 4, 4 vagy 3, 3) 5. Két prímszám (Pl: 13, 17) 6. Az 1 és valamilyen más szám (Pl: 1, 18 vagy 1, 24 vagy 1, 1) 7. A kommutativitás vizsgálata (Pl: 1,18 és 18,1) Érvénytelen tesztesetek: 1. Hibás bemeneti számadatok (<=0) beírása. Programkód szerinti tesztesetek: 1. Beolvasás Különböző bemenő adatok (Pl: 6, 10 illetve 10, 6) A beolvasó elágazás mindkét ágát is kipróbáljuk. (Pl: 6, 8 illetve -3, 5 vagy -4, -7) 2. Főprogram Főprogram ciklusának ellenőrzése: amikor a ciklus egyszer sem fut le (Pl: 2, 2), pontosan egyszer fut le (Pl: 2, 4), többször lefut (Pl: 108, 24) Főprogram ciklusmagjának ellenőrzése: amikor az elágazás mindkét ága egyszer-egyszer biztosan lefut. (Pl: 6, 8) 77

78 Teljes program Tekintsük meg végül a teljes programkódot egyben. A kódot megjegyzésekkel láttuk el, a számításrész ciklusmagjában a kétágú elágazást tömörebb formában írtuk le. A program leállásakor a konzolablak eltűnik, ezért az eredményt csak akkor tudjuk elolvasni, ha a programot vagy egy olyan környezetben futtatjuk, amelyik a konzolablak megszüntetését csak külön felhasználói utasításra végzi el, vagy a program végére valamilyen várakozó utasítást helyezünk el. #include <iostream> using namespace std; int main() { int m, n, d, c; cout << "Legnagyobb közös osztó!" << endl; //Beolvasás: Az m és n beolvasása (m>0, n>0) cout << "Kérem az első számot:"; cin >> m; cout << "Kérem a második számot:"; cin >> n; if(!(m>0 && n>0)){ cout << " Természetes számokkal dolgozok!"; return 1; 78

79 //Főprogram: d:=lnko(m,n) d = n; c = m; while(c!= d){ if (c<d) { d = d-c; else if(c>d) { c = c-d; //Eredmény (d) kiírása cout << endl << "LNKO = " << d << endl; return 0; 79

80 5. Feladat: Legnagyobb közös osztó még egyszer Oldjuk meg újra az előző feladatot, de most az Euklideszi algoritmussal! Változtassunk a specifikáción is: ne válasszuk szét a változóinkat bemenő és eredményváltozókra, hanem az eredmény az egyik bemenő változóban keletkezzen! Olvassunk be két pozitív egész számot billentyűzetről, határozzuk meg a legnagyobb közös osztójukat, majd az eredményt írjuk ki a konzolablakba! Specifikáció A feladatunkban három darab, egész számokat értékül felvevő változó szerepel, ezek között az m és n változók a bemenő változók. Mindkét bemenő változó kezdetben pozitív értéket kell, hogy tartalmazzon: n>0 m>0. A = ( n, m : Z ) Ef = ( n=n m=m n >0 m >0 ) Uf = ( n = lnko(n,m ) ) A célfeltételben szereplő lnko() függvény két egész szám legnagyobb közös osztóját állítja elő. Az eredményt eltérően a 2. feladattól az n változó tesszük. Absztrakt program Az Euklideszi algoritmus helyessége is igazolható az első kötet 3. fejezetében leírtak szerint. Segédváltozóra nincs szükség. m 0 n, m := m, n mod m 80

81 Implementálás A program megvalósítása most is öt lépésben készül. Program kerete A program kerete a korábban megismert forma lesz. #include <iostream> using namespace std; int main() { Deklarációk... return 0; A programban használt változók int típusúak: int m, n Absztrakt program kódolása Az absztrakt program egy ciklus, amelynek magja az n, m := m, n mod m szimultán értékadás. Ez egy segédváltozó segítségével bontható szét három értékadás szekvenciájára: s:=n; n := m; m:= s mod m; while(m!= 0){ int s = n; n = m; 81

82 m = s mod m; Megfigyelhető, hogy a segédváltozót a ciklus magjában deklaráltuk. Ez azt eredményezi, hogy erre a változóra csak a ciklus magjában hivatkozhatunk, mert ez a változó csak a ciklusmag végrehajtásának kezdetén jön létre és a ciklusmag befejeződésekor megszűnik. Ez a ciklusmagra nézve lokális változó. Ezzel szemben az n és m változók, amelyeket eggyel kijjebb, a main függvény blokkjában deklaráltunk, a ciklusmag blokkjára nézve globálisak, használhatóak a ciklusmagban is. Ugyanakkor az n és m változók lokálisak a main függvény blokkjára nézve, hiszen e blokk végrehajtása során jönnek létre, és a blokkból való kilépéskor (a program befejeződésekor) szűnnek meg. Bemenő adatok beolvasása és ellenőrzése Az adatok beolvasása megegyezik a 4. feladat megoldásában látottakkal. A hiba esetén történő leállásához azonban mostantól kezdve nem a return 1, hanem az exit(1) utasítást használjuk (a visszaadott hibakód lehet más érték is). A return ugyanis nem lesz alkalmas a több alprogramra bontott programoknál (lásd II. rész) a meghívott alprogramokban kiváltott leálláshoz. Ezért célszerű már most átszokni az exit() használatára. Megjegyezzük, hogy bizonyos környezetekben az exit() értelmezéséhez szükség lehet az cstdlib modulra, amelyet be kell inklúdolni a programunk elejére. cout << "Legnagyobb közös osztó!" << endl; cout << "Kérem az első számot:"; cin >> m; cout << "Kérem a második számot:"; cin >> n; if(!(m>0 && n>0)){ 82

83 cout << "Természetes számokkal dolgozok!"; exit(1); Eredmény kiírása Az eredmény kiírás csak abban tér el a 4. feladat megoldásától, hogy itt az n változó tartalmazza az eredményt. Tesztelés cout << endl << "LNKO = " << n << endl; A fekete doboz tesztesetek ugyanazok, mint a 4. feladat esetében. A fehér doboz tesztesetek között a beolvasó rész tesztelése is megegyezik a 4. feladatéval. Főprogram ciklusának ellenőrzése: Teljes program amikor a ciklus egyszer sem fut le (Pl: 2, 0), pontosan egyszer fut le (Pl: 2, 2), többször lefut (Pl: 108, 24) Tekintsük meg végül a teljes programkódot egyben. A program leállásakor a konzolablak nem tűnik el, csak egy tetszőleges karakter és az <enter> leütése után. #include <iostream> #include <cstdlib> using namespace std; 83

84 int main() { int m, n; cout << "Legnagyobb közös osztó!" << endl; //Beolvasás: Az m és n beolvasása (m>0, n>0) cout << "Kérem az első számot:"; cin >> m; cout << "Kérem a második számot:"; cin >> n; if(!(m>0 && n>0)){ cout << "Természetes számokkal dolgozok!"; exit(1); //Főprogram: n:=lnko(m,n) while(m!= 0){ int s = n; n = m; m = s % m; //Eredmény (n) kiírása cout << endl << "LNKO = " << n << endl; 84

85 char ch; cin >> ch; return 0; 85

86 C++ kislexikon Típus Jele Értékei Műveletei Megjegyzés Természetes Egész int 83, * / % ==!= < <= > >= egész osztás Valós double , 83e-3 Logikai bool false, true Karakter char a 3 \ + - * / ==!= < <= > >= &&! ==!= ==!= < <= > >= valós osztás Karakterlánc string barmi + [] substr() size() c_str() ==!= < <= > >= Program absztrakt C++ std névtér #include <string> deklaráció változó : típus típus változó; értékadás változó := kifejezés változó = kifejezés szekvencia első program második program első program második program elágazás then ág feltétel else ág if (felétel){ then ág else{ else ág elágazás if (felt1){ felt1 felt2 feltn ág1 ág2 ágn 86

87 többágú ág1 ciklus feltétel mag else if (felt2){ ág2... else if (feltn){ ágn while(felétel){ mag 87

88 3. Tömbök A megoldandó feladataink adatai között gyakran szerepelnek tömbök. A tömb olyan azonos típusú elemek véges sokasága, ahol az elemeket sorozatba (vektor), téglalapba (mátrix), sorozatok sorozatába (változó hosszúságú sorokból álló ún. kesztyű mátrix), esetleg még több dimenziós alakzatba rendezik. Ennek következtében egy tömb egy elemére indexszel, az elemnek az alakzatban elfoglalt pozíciójának sorszámával (több dimenziós esetben több indexszel) lehet hivatkozni: kiolvashatjuk illetve megváltoztathatjuk az így megjelölt értékeit. Egy tömb dimenzióinak száma is, a dimenziók mérete (hossza) is rögzített, nem változtatható, ennél fogva a tömb elemeinek száma is állandó. A tömböket használó absztrakt programok kódolása nem bonyolult, hiszen a legtöbb magas szintű programozási nyelv biztosítja a tömbök használatát, és az absztrakt programban használt tömbökkel kapcsolatos jelölések is hasonlóak a programozási nyelvekéhez. Az absztrakt programokkal ellentétben azonban a futtatható programban létre is kell hozni egy tömböt, fel kell tölteni értékekkel, ki kell tudnunk írni ezeket az értékeket a szabványos kimenetre. Implementációs stratégia A tömbök alkalmazása esetén az egyik fontos kérdés az, hogy a választott programozási nyelv megengedi-e a tömbök elemeinek tetszőleges indexelését vagy nem. Például a C, C++, C#, Java nyelvekben egy tömb elemeit csak nullával kezdődően indexelhetjük. Ha az absztrakt programban használt tömb nem ilyen, akkor az implementálás során átalakítást kell végezni az absztrakt programon. A fent említett nyelvekben egy m..n intervallummal indexelt vektort egy 0..n m intervallummal indexelt egy dimenziós tömbként lehet csak ábrázolni. Ezért a kódoláskor azokban a programrészekben, ahol a tömb feldolgozása az m..n intervallumra van megfogalmazva (például egy ciklusban) vagy át kell térni a 0..n m intervallumra, vagy az absztrakt programban i-edik tömbelemként megjelölt értékre i m indexszel kell hivatkozni. A beolvasó illetve kiíró részekben a felhasználó felé viszont mindig a specifikációban szereplő indextartománnyal 88

89 kell a tömböt megjeleníteni. Tehát ha a kódban szereplő 0..n m intervallummal indexelt tömb a specifikáció szerint egy m..n intervallummal indexelt vektor, akkor a kódbeli tömb i [0..n m]-edik elemének kiírásakor azt kell mutatnunk, hogy ez a vektor i+m-edik eleme. általános tömb: m i n 3-1. C - ábra. stílusú Általános tömb: indexelésű tömb megfeleltetése 0-tól indexelt tömbnek 0 i m n m+1 A tömbök implementálásánál lényeges kérdés az is, hogy az adott programozási nyelven már fordítási időben (azaz kódba égetett módon) rögzítjük-e a tömb méretét vagy ezt futási időben adjuk-e meg. Speciális, fordítási időben megadott méretű tömb az úgynevezett konstans tömb is, amelyiknek nemcsak a méretét, hanem az elemeit is fordítási időben (tehát a kódban) rögzítjük. Ennél jóval gyakoribb eset az, amikor egy tömb elemeit a futási időben (a felhasználótól bekérve) kapjuk meg. Ha a tömb méretét is a felhasználótól várjuk, akkor a fordítási időben rögzített tömb helyett a futási időben történő tömb-létrehozás az előnyösebb. Ha azonban az adott programozási környezet ezt nem teszi lehetővé (Pascal), de előre tudható, hogy a tömb lehetséges méreteinek mi a várható maximuma, akkor létrehozhatunk ezzel a maximális mérettel egy (már fordítási időben) rögzített méretű tömböt, amelynek az első valahány elemét fogjuk csak futási időben feltölteni és használni, és természetesen külön eltároljuk majd a tényleges elemek számát. C++ nyelven a fenti lehetőségek mindegyike megvalósítható. A programozási nyelvekben találkozhatunk olyan tömbmegvalósításokkal is, ahol egyáltalán nem tömbökre jellemzően a tömb változtathatja a méretét a létrehozása után is: hozzá lehet fűzni új elemet, el lehet hagyni a végéről elemeket. Ha valóban tömb szerepel a megoldandó 89

90 feladatban, akkor az absztrakt megoldás kódjában nincs szükségünk a méretváltoztatással járó műveletekre. Ugyanakkor ezek a műveletek (elsősorban az új elem hozzáfűzése) igen kényelmessé teszik a tömb létrehozását, a méret folyamatos növelése mellett történő kezdeti feltöltését, tehát az adatbeolvasási szakaszban érdemes megengedni ezt a nem tömbszerű viselkedést. konstans méretű tömb: A programkódban adjuk meg a tömb méretét, így az már fordítási időben rögzített. megadható méretű tömb: A program futása során adjuk meg a tömb méretét, amely ettől kezdve természetesen már nem változhat. változtatható méretű tömb: A program futása során adjuk meg a tömb méretét, amelyet meg lehet változtatni. Elméleti szempontból ez nem is tekinthető igazi tömbnek ábra. Tömb méretének megadási lehetőségei A tömbök használata során ajánlott kialakítani olyan kódolási szokásokat, amelyek alapján mindig egyformán hozzuk létre, töltjük fel és írjuk ki a tömbjeinket. Ezek tulajdonképpen olyan kódrészletek, kódminták, amelyeket minimális változtatással lehet újra és újra felhasználni az újabb feladatok kódolásánál. Mivel ezek a kódminták az úgynevezett számlálós ciklust tartalmazzák, ezért a kódolásnál is célszerű a választott programozási nyelv ehhez legjobban illeszkedő nyelvi elemét használni. Nyelvi elemek A tömbök definiálására az egyes programozási nyelvek különféle megoldásokat nyújtanak. Az alkalmazások szempontjából nekünk elég azt vizsgálni, hogy egy tömb mérete fordítási időben vagy futási időben rögzül-e, esetleg futás közben változtatható méretű lesz-e. Egy tömböt a nevének, elemi értékei típusának és méretének megadásával deklarálhatunk. A tömb elemei azonos méretű memória 90

91 szeleteken sorban egymás után helyezkednek el a helyfoglalás módjától függő valamelyik memóriaszegmensben. C++ nyelven a legegyszerűbb mód egy egydimenziós tömb deklarációjára az alábbi. int v[34]; Ez egy 34 darab egész szám tárolására alkalmas tömböt foglal le, ahol az első elem elöl, az utolsó elem hátul foglal helyet. A C-szerű nyelveknél bevezetett tömb elemeit mindig nullától kezdődően indexeljük, tehát a példában szereplő tömb első eleme a v[0], utolsó eleme a v[33]. Az, hogy ez fordítási idejű vagy futási idejű definíciót jelent-e attól függ, hogy hol helyezzük el. A main függvény törzsében elhelyezve futási időben kiértékelt definíció lesz, éppen ezért nem kell a méretét konstansként megadni. int n; cin >> n; int v[n]; Erről a megoldásról azonban tudni kell, hogy nem C++ szabvány (csak C99), viszont a g++ fordító ismeri. A könyv második és harmadik részében már nem fogjuk használni. A tömbökkel kapcsolatos legfontosabb művelet az adott indexű elemre történő hivatkozás. Ha v egy tömb, akkor v[i] a tömb i-edik elemét jelöli: annak értékét ki lehet olvasni és meg lehet változtatni. A v[i] szerepelhet értékadás mindkét oldalán illetve kifejezésekben. Fontos tulajdonság még a tömb mérete, amelyet sok esetben külön kell a programozónak tárolni, mert nem érhető el közvetlenül. Vigyáznunk kell arra, hogy a tömböt ne indexeljük túl, azaz ne hivatkozzunk nem létező indexű elemére. Sajnos erre egy C++ kód fordításakor és futtatásakor semmi nem figyelmeztet, csak közvetett módon a nem várt működés. A programozó feladata, hogy ellenőrizze, hogy az általa megadott index valóban a tömb valamelyik elemére mutat-e, nem következik-e be indextúlcsordulás, mint például egy 34 elemű tömb esetében a v[67] hivatkozás esetén. 91

92 C++ nyelven változtatható méretű tömböt definiálhatunk a vector<> típus segítségével. A vector<> típus megvalósításának hátterében dinamikus memóriakezelés történik, de ennek kódja el van rejtve, azzal nincs semmi dolgunk, nem kell vele foglalkoznunk. A kisebb-nagyobb jelek között lehet megadni az elemek típusát (vector<int> vagy vector<double>), utána a tömbváltozó nevét, és a név után gömbölyű zárójelek között a tömb méretét, bár ez elmaradhat és később is megadható. int n; cin >> n; vector<int> v(n); Változók helyfoglalásának módjai A program változóit meg szokták különböztetni aszerint, hogy deklarációjukra és helyfoglalásukra rögtön a program futásának kezdetekor kerül sor (statikus helyfoglalás) vagy a futás közben. Az előbbi esetben a változó élettartama az egész futási időre kiterjed, az utóbbi esetben csak annak egy szakaszára. Ha a helyfoglalás a deklarációjának végrehajtásakor történik, megszűnése az élettartam végét jelző pontnál (például a deklarációt tartalmazó blokk végén) következik be, akkor automatikus helyfoglalásról beszélünk. Ha a deklaráció és a helyfoglalás időben különválik, és a helyfoglalás külön utasításra jön létre és külön utasításra szűnik meg, akkor dinamikus helyfoglalásról van szó. A program számára kijelölt memória szegmensek közül a statikus helyfoglalás az úgynevezett adatszegmensben, az automatikus helyfoglalás a verem szegmensben (stack), a dinamikus helyfoglalás a szabad vagy dinamikus memóriaszegmensben (heap) történik. Ezek a tárolási kategóriák a tömbökre is érvényesek. Tömb statikus helyfoglalásához előre, még a program futtatása előtt, azaz már fordítási időben ismerni kell a tömb méretét, és mindehhez a tömböt speciális módon (helyen) kell definiálni. Ennek hiányában (például egy blokkba ágyazott tömb deklaráció esetén) a tömb automatikus helyfoglalású lesz, ami a deklaráció végrehajtásakor következik be. Ha még ez előtt lehetőségünk van arra, hogy 92

93 futási időben kiszámoljuk (esetleg beolvassuk) a tömb méretét, akkor futási időben megadható automatikus tömb helyfoglalásról beszélhetünk. Tömb dinamikus helyfoglalása esetén ellenben a programozónak egy úgynevezett (memória címet tároló) pointerváltozót kell definiálnia először, majd külön utasítással kell a tömb elemei számara (sorban egymás után) lefoglalni a szükséges helyet a szabad memóriában, végül e helyfoglalás kezdőcímét a pointerváltozónak kell értékül adni. Ezt a pointerváltozót kvázi tömbként használhatjuk, azaz indexelésével hivatkozhatunk a tömb elemeire. A lefoglalt terület felszabadítása nem feltétlenül automatikus, azt vagy a felhasználónak kell egy külön utasítással kezdeményezni, vagy bizonyos nyelveknél a háttérben működő hulladék-gyűjtő mechanizmus (garbage collector) végzi el. A vector<> típus számos olyan művelettel is rendelkezik, amelyek miatt elméleti szempontból már nem is lenne szabad vektornak nevezni. Ilyen az átméretezést végző resize(), vagy a tömb végéhez új elemet hozzáfűző push_back() művelet. Lehetőség van egy vektor elemeinek olyan formában történő indexelésére is, amelyik figyeli az indextúlcsordulást. A vector-ral létrehozott tömbök hasonlatosak karakterláncokhoz (string), csak azok kizárólag karaktereket tartalmazhatnak, és olyan műveleteket is végrehajthatunk, mint például egy részlánc kivágása. A karakterláncban a karakterekre a tömbökhöz hasonlóan indexeléssel hivatkozhatunk, és hozzáfűzhetünk, el is hagyhatunk belőle karaktereket. Többdimenziós tömbök kezelése nem nagyon tér el az egydimenziósokétól. Ugyanis például egy mátrix felfogható sorok egydimenziós tömbjének, a sorok pedig maguk is egydimenziós tömbök. Rögzített méretű n m-es mátrixot definiál futási időben az alábbi kódrészlet (nem C++ szabvány): int n, m; cin >> n >> m; int t[n][m]; 93

94 Ugyanez vector<> típussal egy kicsit körülményesebb. Először a sorok számát kell megadni, majd soronként ez egyes sorok hosszát, viszont lehetőség van eltérő sorhosszú sorok megadására is: int n, m; cin >> n >> m; vector<vector<int>> t(n); for(int i=0; i<n; ++i) t[i].resize(m); A t[i][j] a mátrix i-edik sorának j-edik elemét jelöli, a t[i] a mátrix i-edik sorát azonosítja. Az úgynevezett számlálós ciklus egy speciális szerkezetű programot jelöl. Valójában ez egy szekvencia, amelynek a második része egy olyan ciklus, amely előre kiszámítható alkalommal futtatja le a ciklus magot. A számlálós ciklusok egy úgynevezett ciklusváltozót (futóindexet) vezetnek végig egy egész intervallumon, és sorban annak minden értékére végrehajtják a ciklusmagot. A ciklusváltozó értéke nemcsak egyesével változtatható, és készíthető a futóindexet csökkentő számlálós ciklus is. i:=1 i n vagy i:=1..n ciklusmag másképp ciklusmag i:=i+1 jelölve A legtöbb nyelv rendelkezik a számlálós ciklust kódoló speciális utasítással, sőt vannak olyan nyelvek, amelyek csak ezt ismerik, az általános elöl tesztelős ciklust nem. A C, C++, C# és Java nyelvek for ciklusa jóval általánosabb nyelvi elem, mint ami egy számlálós ciklus kódolásához kell. A for(eleje;feltétel;továbblépés){ ciklusmag utasítást ugyanis általában az alábbi programrész kódolására használhatjuk. 94

95 eleje feltétel ciklusmag továbblépés A klasszikus számlálós ciklust a for ciklus egy speciális változatával, a for(int i=1;i<=n;++i){ ciklusmag utasítással kódolhatjuk. Ebben a ciklusváltozót a for ciklus belső, lokális változójaként definiálhatjuk. Természetesen az is könnyen előírható, hogy ne növekedjen, hanem csökkenjen, és ne egyesével, hanem nagyobb léptékben. 95

96 6. Feladat: Tömb maximális eleme Adjunk meg egy tömbben egész számokat, keressük meg a tömb valamelyik maximális elemét, és az eredményt írjuk ki a szabványos outputra. Készítsünk többféle megoldást attól függően, hogy a tömböt hogyan hozzuk létre! Specifikáció A feladat a tömb elemei felett végzett maximum kiválasztás. Adott tehát egy egészeket tartalmazó tömb, amelynek elemeit (mivel a feladat szempontjából ez tűnik logikusnak) 1-től kezdődően indexeljük n-ig, ahol az n a tömbre jellemző rögzített értékű természetes szám. Az n értéke legalább 1 kell legyen, hiszen a maximum kiválasztás csak akkor értelmezhető, ha a tömbnek van legalább egy eleme. A célunk az, hogy meghatározzuk a tömb legnagyobb elemét és ennek az indexét. (Ha a legnagyobb elem több helyen előfordul a tömbben, akkor mondjuk az első előfordulás indexét keressük.) A = ( v : Z n, max, ind : Z) Ef = ( v=v n>0 ) Uf = ( v=v n max = v[ind] = max i 1 v[i] ind [1..n] ) n Az utófeltételben a max i 1 legnagyobb elemét adja meg. v[i] jelölés, a {v[1], v[2],, v[n] halmaz Absztrakt program Egy tömb elemei felett végzett maximum kiválasztás programját már a kezdő programozók is jól ismerik. max, ind := v[1], 1 i = 2.. n v[i]>max max, ind := v[i], i SKIP 96

97 (Ennek a programnak az előállítását könyvünk első kötete tartalmazza). Vegyük észre, hogy a program egy speciális (úgynevezett számlálós) ciklusból áll, amelynek segédváltozója az i : N ciklusváltozó. Az n : N önálló változónak tűnik, de itt ez a v tömb tartozéka, a tömb elemeinek számát jelöli. Implementálás A megoldó programot négyféleképpen fogjuk implementálni. Az egyes megoldások azonban csak a tömb definiálásában térnek majd el egymástól, és ennek csak a beolvasás módjára lesz hatása. A program szerkezete, az absztrakt program kódja és az eredmény kiírása ellenben mindegyik esetben azonos lesz. Nézzük meg először a közös részek kódját. A program kerete A main.cpp-ben helyezzük el a main függvényt. #include <iostream> using namespace std; int main() { // Tömb definiálása és feltöltése... // Maximum kiválasztás... // Eredmény kiírása... return 0; 97

98 Absztrakt program kódolása Az absztrakt program számlálós ciklusának kódoláshoz a for ciklust használjuk. Ennél figyelembe vesszük azt, hogy a kódban az n elemszámú tömb indexeinek alsó határa 0, a felső határa pedig az n 1 lesz, szemben az absztrakt programban használtakkal. Ennek megfelelően a maximum kiválasztásnál az első megvizsgálandó elem nem a v[1], hanem a v[0] lesz, a számlálós ciklus intervalluma pedig a 2..n helyett az 1..n 1. int ind = 0, max = v[0]; for(int i=1; i<n; ++i){ if(v[i]>max){ ind = i; max =v [i]; Eredmény kiírása Ha az eredményt kiíró kódban a megtalált elem indexéhez, az ind-hez egyet hozzáadunk, akkor azt a látszatot kelthetjük, mintha a tömb nem 0-tól kezdődően lenne indexelve, hanem 1-től. Természetesen a tömb feltöltésénél is hasonlóan kell majd eljárni: amikor az i+1-dik elemet kérjük, akkor azt i-edik elemként kell tárolni. cout << "A tömb legnagyobb eleme: " << max << ",amely a " << (ind+1) << ". elem.\n"; 98

99 Tömb feltöltése Az alábbiakban többféle tömb megadási módot mutatunk be. Az alábbi változatok az eddig megadott kódokra nincsenek hatással, azok minden esetben használhatók. 1. Implementálás konstans tömbbel A konstans tömb elemeit a program szövegébe ágyazva, fordítási időben adjuk meg. Ha más adatokkal akarunk dolgozni, akkor módosítanunk kell a program szövegén, és azt újra le kell fordítanunk. A v tömb típusú változó definiálásánál meg kell adnunk a tömb elemeinek a típusát és a tömb elemeinek számát (a tömb hosszát vagy méretét). A tömb első eleme mindig a 0 indexet kapja, ezen nem tudunk változtatni. A tömb hossza implicit módon is megadható, ha a definícióban felsoroljuk a tömb elemeit, ezért nem kell beírni az alábbi kódban a szögletes zárójelek közé a 8-as méretet. Ez a tömb attól lesz konstans, azaz megváltozhatatlan, ha a const kulcsszóval jelöljük meg, ami nem engedi (fordítási idejű ellenőrzés), hogy később megváltoztassuk az elemeit. const int v[] = {4,7,0,9,6,7,9,4; A tömb hosszát külön is érdemes eltárolni, ugyanis ez a legegyszerűbb módja annak, ha a méretre később szükségünk lesz. Erre alkalmas egy const int n = 8; konstans értékű segédváltozó, de a programozó felelőssége az, hogy ennek értéke szinkronban legyen a tömb méretével. Szerencsésebb megoldás, ha a tömb méretét kiszámoljuk a tömb által lefoglalt teljes memória méret és bármelyik (például az első) tömbelem által lefoglalt memória méret hányadosaként. Természetesen ennek csak a v tömb definiálása után van értelme. const int n = sizeof(v)/sizeof(v[0]); 99

100 A maximum kiválasztás esetében figyelni kell arra is, hogy a ciklus előtti v[0] hivatkozás értelmes legyen. Ezt az előfeltétel ugyan biztosítja, de ennek a beolvasás részben kell érvényt szerezni. Ezért illesszük be az alábbi hibakezelő elágazást a kódba. if (0 == n){ cout << "Nincs a tömbnek eleme!\n"; return 1; Célszerű végül egy for ciklussal kiírni a tömb elemeit a szabványos kimenetre, hogy a felhasználó lássa a kódba égetett tömb elemeit. Egy n elemű tömb kiíratásánál a 0..n 1 intervallumot kell egy futóindexszel bejárni, és minden lépésben a tömb i-edik elemét kiírni. Erre, ha a ciklusmag egyetlen utasításból áll, akkor használható az alábbi forma is: for(int i=0; i<n; ++i){ utasítás; Ennek megfelelően tehát cout << "A tömb elemei: " << endl; for (int i=0; i<n; ++i) cout << v[i] << " "; cout << endl; Nézzük meg egyben az egész kódrészletet: const int v[] = {4,7,0,9,6,7,9,4; const int n = sizeof(v)/sizeof(v[0]); if (0 == n){ 100

101 cout << "Nincs a tömbnek eleme!\n"; return 1; cout << "A tömb elemei: " << endl; for (int i=0; i<n; ++i) cout << v[i] << " "; cout << endl; A kitűzött feladat megoldásánál a konstans tömb használata nem látszik célszerűnek, hiszen annak sem méretét, sem elemeit nem ismerjük a program megírásakor. A bemutatott megoldás csak didaktikai megfontolásból került ide. 2. Implementálás maximált méretű tömbbel Készítsünk olyan tömböt, amelynek elemeit a program futása során kell beolvasnunk a szabványos bementről. A tömb méretét viszont még fordítási időben az előtt definiáljuk, mielőtt tudnánk, hogy ténylegesen hány elemet akarunk elhelyezni benne. A v tömb típusú változó definiálásánál meg kell adnunk a tömb elemeinek a típusát és a tömb elemeinek számát. Mivel a pontos elemszámot nem tudjuk, választunk egy megfelelőnek látszó maximális méretet, amit a programban konstansként veszünk fel. Ezután deklarálunk egy ilyen hosszú (pl. 100 elemű), egész számokat tartalmazó tömböt. const int maxsize = 100; int v[maxsize]; Ez a deklaráció egyenértékű az int v[100]-zal. A maxsize konstans használata azért előnyösebb, mert ha a programban mindenhol következetesen ezt használjuk a 100 érték helyett, akkor egy esetleges 101

102 program-módosításnál elegendő lesz csak a maxsize kezdőértékét megváltoztatni. A v tömbben legfeljebb maxsize darab elem helyezhető el. Ha kevesebb elemet tárolunk benne, akkor azok a tömb elejére kerülnek. Ilyenkor szükség van a tömb tényleges hosszának tárolására, ehhez bevezetünk egy egész típusú változót. Legyen a neve: n. A tömb beolvasása a tömb tényleges hosszának megadásával kezdődik. Ennek az érteke nem lehet negatív, és nem lehet nagyobb a maxsize értékénél sem. Ezért a beolvasáskor a feladat előfeltételéből származó n>0 feltétel mellett az n<=maxsize feltételt is vizsgálni kell. const int maxsize = 100; int v[maxsize]; int n; cout << "Adja meg a tömb elemszámát, amely 1 és " << maxsize << " közé kell essen: "; cin >> n; if(!(n>0 && n<=maxsize)){ cout << "Tömb hossza nem megfelelő!\n"; return 1; Ezek után sor kerülhet a tömb elemeinek beolvasására. Látszik, hogy a beolvasásnál figyelünk a nullától induló indexelésre: az i+1-dik elemet i-edik elemként tároljuk, így a beolvasáskor a felhasználóban azt a látszatot keltjük, mintha 1-től indexelt tömbünk lenne. Az alábbi kód feltölti a tömböt a billentyűzetről. Most nincs arra szükség, hogy külön ki is írjuk a tömb elemeit, mint azt a konstans tömböknél láttuk, hiszen a konzolablakban még látszanak a felhasználó által beírt értékek. 102

103 cout << "Adja meg a tomb elemeit!\n"; for(int i=0; i<n; ++i){ cout << i + 1 << ". elem: "; cin >> v[i]; Az itt bemutatott megoldás rugalmasabb az előzőnél, de van egy nagy hiányossága: ha több elemet akarunk elhelyezni a tömbben, mint annak korábban megadott hossza, akkor nem oldható meg a feladat, ha kevesebbet, akkor feleslegesen foglal memóriahelyet. A következő két pontban azt mutatjuk meg, hogyan lehet mindig pont akkora tömböt lefoglalni, amekkorára éppen szükség van. 3. Implementálás futás közben megadott méretű automatikus helyfoglalású tömbbel Érdekes lehetőséget kínál a tömbök definiálására az a nem C++ szabvány szerinti megoldás, amikor a tömb méretét a deklarációban egy futás közben kiértékelhető kifejezéssel adjuk meg. Erre több féle lehetőség is van. A most bemutatott változat a legegyszerűbb (erre utal az automatikus helyfoglalású jelző), de egyszersmind a legkorlátozottabb is. int n; cin >> n; int v[n]; Részletesebben int n; cout << "Adja meg a tömb hosszát (1 <= n ): "; 103

104 cin >> n; if(!(n>0)){ cout << "Tömb hossza legalább 1 legyen!\n"; return 1; int v[n]; A tömb elemeinek beolvasása azonos a maximált méretű tömbnél látottakkal. cout << "Adja meg a tömb elemeit!\n"; for(int i=0; i<n; ++i){ cout << i + 1 << ". elem: "; cin >> v[i]; Fontos megjegyezni, hogy ez a megoldás nem alkalmazható olyan több függvényre tagolt programok esetén, ahol az egyik függvénynek ez a szerepe, hogy amikor meghívják, akkor visszaadjon egy ilyen futás közben megadott méretű tömböt. Ehhez dinamikus helyfoglalású tömb kell, de ezt csak a pointerek megismerése után tudjuk bemutatni a III. részben. Létezik azonban egy olyan lehetőség is, amely a dinamikus helyfoglalású tömbök használatát teszi lehetővé anélkül, hogy a pointerek világában el kellene merülnünk. Erről szól az alábbi pont. 4. Implementálás vector<>-ral A legrugalmasabb megoldást kínálja C++ nyelven a szabványos sablonkönyvtár (STL) vector típusa. Ennek segítségével futási idő alatt 104

105 adható meg a lefoglalni kívánt tömb mérete, ráadásul egyéb kényelmes szolgáltatáshoz is hozzásegít: például bármikor lekérdezhető a tömb mérete vagy megváltoztatható a méret használat közben. A vector<típus> segítségével egy tetszőleges típusú adott hosszúságú tömböt tudunk létrehozni, int n; cin >> n; vector<int> v(n); de lehetőség van a méret későbbi megadására is. A vector<int> v; egy nulla hosszúságú vektort definiál, amelynek a mérete vagy a v.resize(új méret) utasítással, vagy v.push_back(elem) utasítással növelhető. Az előbbi kívánt méretűre módosítja a vektor hosszát, az utóbbi egy új elemet fűz a végére, azaz eggyel növeli a méretét. vector<int> v; int n; cout << "Adja meg a tömb hosszát (1 <= n ): "; cin >> n; if(!(n>0)){ cout << "Tömb hossza legalább 1 legyen!\n"; return 1; v.resize(n); 105

106 A tömb aktuális mérete bármikor lekérdezhető a size() függvény segítségével. Ez a függvény egy úgynevezett előjel nélküli egész számként adja vissza a tömb hosszát, és ennek egész számként való értelmezése figyelmeztetést vált ki a fordításkor. Ez elkerülhető, ha a size() függvény értékét egész számmá alakítjuk (explicit konverzió). int n = (int)v.size(); A tömb elemeinek beolvasása azonos a maximált méretű tömbnél látottakkal. int n = (int)v.size(); cout << "Adja meg a tomb elemeit!\n"; for(int i=0; i<n; ++i){ cout << i + 1 << ". elem: "; cin >> v[i]; Tesztelés A fekete doboz teszteseteket kizárólag a feladat alapján, a megoldó program ismerete nélkül készítjük, de a tesztelésre csak az implementáció után kerülhet sor. Itt azokat a bemenő adat-eseteket próbáljuk felderíteni, amelyek a feladat szempontjából lényegesek illetve szélsőségesek. Az előbbihez a specifikáció utófeltételét, az utóbbihoz az előfeltételét vesszük elsősorban figyelembe. Most itt ilyen eseteket sorolunk fel anélkül, hogy egyegy esethez konkrét adatértékeket megadnánk. Érvényes tesztesetek: 1. Nulla darab szám esete. 106

107 2. Egyetlen szám esete. 3. Sok különböző szám esete. 4. A megadott számok között több azonos forduljon elő a maximális értékből. 5. Legnagyobb szám az első helyen álljon. 6. Az 4. és 5. esetek ötvözése. 7. Legnagyobb szám az utolsó helyen álljon. 8. Az 4. és a 7. esetek ötvözése. 9. Általános eset. Érvénytelen teszteset: 1. Nulla vagy negatív hosszú tömb. A fehér doboz tesztelés azoknak a teszteseteknek az összegyűjtését jelenti, amelyek azt biztosítják, hogy a program minden utasítása legalább egyszer végrehajtódjon. Nézzük meg először a tömb feltöltésért felelős kódot. Ennek tesztelésénél az alábbi esetekre kell tesztadat-sort készíteni: 1. Tömb hosszának helyes megadása. (n=0, n<0 esetek kipróbálása) 2. Egy hosszú tömb esete. 3. Több elemű tömb esete. 4. Maximált méretű tömb használata esetén olyan méretre is ki kell próbálni a programot, amely eléri illetve nagyobb, mint a megengedett maximális méret. A maximum kiválasztás kódjának tesztelésénél olyan adatokat kell választani, amelyekkel az alábbi futtatások érhetőek el. 5. Egyszer sem lép be a vezérlés a ciklusmagba. 6. Csak egyszer hajtódik végre a ciklusmag. 7. Először a baloldali ága hajtódik végre a ciklus magnak. 8. Először a jobboldali ága hajtódik végre a ciklus magnak. 9. Mindig csak a baloldali ága hajtódik végre a ciklus magnak. 10. Mindig csak a jobboldali ága hajtódik végre a ciklus magnak. 107

108 108

109 Teljes program Tekintsük meg végül egyben a vector<>-t használó teljes programkódot. Itt nem vezetjük be az aktuális méretet mutató n változót, helyette az (int)v.size() kifejezést használjuk. #include <iostream> #include <vector> using namespace std; int main() { // Vektor definiálása vector<int> v; int n; cout << "Adja meg a tömb hosszát (1 <= n ): "; cin >> n; if(!(n>0)){ cout << "Tömb hossza legalább 1 legyen!\n"; return 1; v.resize(n); // A vektor elemeinek feltöltése cout << "Adja meg a tömb elemeit!\n"; 109

110 for (int i=0; i<(int)v.size(); ++i){ cout << i + 1 << ". elem: "; cin >> v[i]; // Maximum kiválasztás int ind = 0, max = v[0]; for(int i=1; i<(int)v.size(); ++i){ if (v[i]>max){ ind = i; max = v[i]; // Eredmény kiírása cout << "A tömb legnagyobb eleme: " << max; cout << ",amely a " << (ind+1) << ". elem.\n"; return 0; 110

111 7. Feladat: Mátrix maximális eleme A földfelszín egy téglalap alakú darabján megmértük a tengerszint feletti magasságokat, és azokat egy mátrixban rögzítettük. Melyik a terület legmagasabb pontja, azaz melyik a mátrix legnagyobb eleme, hányadik sorban és oszlopban található? Specifikáció A feladat hasonlít az előzőre, de most egy kétdimenziós alakzatban kell maximumot keresni. A = ( t : Z n m, max, ind, jnd : Z ) Ef = ( t=t n>0 m>0 ) n, m Uf = ( t=t max = t[ind,jnd] = max t[ i, j] i 1, j 1 Absztrakt program ind [1..n] jnd [1..m] ) Egy mátrix elemei feletti maximum kiválasztás az elemek bejárására két egymásba ágyazott ciklust használ. Egyik a sorokon vezet végig egy segédváltozót, a másik az oszlopokon. max, ind, jnd := t[1,1], 1, 1 i = 1..n j = 1..m t[i,j]>max max, ind, jnd := t[i,j], i, j SKIP Implementálás A program kerete a korábban megismert hérom részre tagolt formájú lesz: beolvasás, számolás (maximum kiválasztás) és kiírás részeket fog tartalmazni. 111

112 #include <iostream> #include <vector> using namespace std; int main() { // Mátrix definiálása és feltöltése... // Maximum kiválasztás... // Eredmény kiírása... return 0; Mátrix definiálása és feltöltése Nemcsak egydimenziós tömböket lehet C++ nyelven definiálni, hanem két- (mátrixok), sőt többdimenziósakat is. Amennyiben a többdimenziós tömb fogalmával tisztában vagyunk, akkor a C++ nyelvű használatuk nem jelenthet gondot. Például az int t[n][m]utasítás egy n m-es egész számokat tartalmazó mátrixot definiál. A mátrix méretei futási időben is megadhatók. A mátrix i-dik sorának j-dik elemét jelöli a t[i][j] szimbólum, de lehetőségünk van t[i]-vel a teljes i-edik sorra hivatkoznunk. A mátrix feltöltését és kezelését két egymásba ágyazott for ciklussal végezzük. A vector<> típus segítségével is lehet mátrixokat definiálni. Sőt, ez arra is lehetőséget ad, hogy változó sorhosszúságú, úgynevezett kesztyű mátrixokat hozzunk létre (igaz, ebben a feladatban téglalap alakú mátrixra 112

113 van szükség), mert a kesztyűmátrix különböző méretű vektoroknak a vektora. Egy n m-es téglalap alakú mátrixot hoz létre az alábbi kódrészlet. int n, m; cin >> n >> m; vector< vector<int> > t(n); for(int i = 0; i<(int)t.size(); ++i) t[i].resize(m); Az is látható a fenti kódból, hogy ha akarnánk, hogyan adhatnánk minden sornak más-más méretet. A mátrix feltöltése két egymásba ágyazott for ciklussal történik. A ciklusfeltételhez a size() függvénnyel kérdezzük le a tömb méreteit. Ügyelünk arra is, hogy bár a C++ 0-tól kezdődően indexeli a tömböket, a felhasználó egy 1-től indexelt mátrixot lásson. for(int i = 0; i<(int)t.size(); ++i) for(int j = 0; j<(int)t[i].size(); ++j) { cout << "t[" << i+1 << "," << j+1 << "]= "; cin >> t[i][j]; Maximum kiválasztás Az absztrakt program kódjában a mátrix sorainak és oszlopainak indextartományát át kell alakítani a 0-től indexelt mátrixra. Formailag ugyanaz a két ciklus szerepel itt is, mint a mátrix feltöltésénél. ( A beolvasás és a maximum kiválasztás dupla ciklusait össze is lehetne vonni, de akkor tömbre sem lenne szükség, márpedig itt ennek a használatát mutatjuk be.) int ind = 0, jnd = 0, max = t[0][0]; 113

114 for(int i = 0; i<(int)t.size(); ++i) for(int j = 0; j<(int)t[i].size(); ++j) { if(t[i][j]>max){ ind = i; jnd = j; max = t[i][j]; Eredmény kiírása A kiírásnál ügyelünk arra, hogy a felhasználó 1-től indexelt mátrixot lásson. cout << "A mátrix legnagyobb eleme: " << max << ",amely a " << ind+1 << ". sor " << jnd+1 << ". eleme.\n"; Tesztelés Az előző feladat tesztelése alapján készíthetők erre a megoldásra is tesztesetek. Ezt az Olvasóra bízzuk. Teljes program Tekintsük meg végül egyben a teljes programkódot. #include <iostream> #include <vector> using namespace std; 114

115 int main() { // Mátrix definiálása int n, m; cout << "A mátrix sorainak száma (1 <= n ): "; cin >> n; cout << "A mátrix oszlopainak száma (1 <= m ): "; cin >> m; if(!(n>0 && m>0)){ cout << "Méret legalább 1 1-es legyen!\n"; return 1; vector<vector<int> > t(n); for(int i=0; i<n; ++i) t[i].resize(m); // A mátrix elemeinek feltöltése cout << "Adja meg a mátrix elemeit!\n"; for(int i = 0; i<(int)t.size(); ++i) for(int j = 0; j<(int)t[i].size(); ++j) { cout << "t[" << i+1 << "," << j+1 << "]= "; cin >> t[i][j]; // Maximum kiválasztás int ind = 0, jnd = 0, max = t[0][0]; 115

116 for(int i = 0; i<(int)t.size(); ++i) for(int j = 0; j<(int)t[i].size(); ++j) { if(t[i][j]>max){ ind = i; jnd = j; max = t[i][j]; // Eredmény kiírása cout << "A mátrix legnagyobb eleme: " << max << ",amely a " << ind+1 << ". sor " << jnd+1 << ". eleme.\n"; return 0; 116

117 8. Feladat: Melyik szóra gondoltam Találd ki melyik szóra gondoltam! A programot két felhasználó kezeli. Az egyik, ő a játékmester, kitalál egy szót, és azt begépeli úgy, hogy az a másik felhasználó számára rejtve maradjon. A másik felhasználó, a játékos, megpróbálja ezt a számára elrejtett szót kitalálni úgy, hogy egymás után többször is tippel, az alkalmazás pedig jelzi, hogy mely betűket sikerült eltalálnia. A program a játékos tippjére egy olyan szóval válaszol, amely azokon pozíciókon, ahol a tipp és a rejtett szó betűje megegyezik, az adott betűt mutatja, a többi pozíción pedig csak egy-egy pontot jelenít meg. A program számolja azt is, hogy hány próbálkozásból sikerült megfejteni a rejtett szót! Specifikáció A feladat megoldása azzal kezdődik, hogy beolvassuk a játékmestertől az elrejtendő szót. Ekkor inicializáljuk a próbálkozások számát nullára. Ezt követően ugyanannak a tevékenységnek az ismétlése következik. Ez a tevékenység az alábbi feladatot oldja meg: Olvassunk be egy karakterláncot (hívjuk ezt tippnek) a játékostól. Növeljük meg a próbálkozások darabszámát eggyel. Ha a tipp megegyezik az előzetesen elrejtett szóval vagy egy speciális, a játék feladását jelentő x üzenettel, akkor ezt visszaigazoljuk. Ha a tipp hosszabb, mint a rejtett szó, akkor vágjuk le a végét; ha rövidebb, egészítsük ki annyi ponttal, hogy a hossza megegyezzen a rejtett szó hosszával. Ezután a tipp minden olyan betűje helyére is írjunk pontot, amelyik nem azonos a rejtett szó ezen pozícióján levő betűvel. Specifikáljuk azt a résztevékenységet, amely feltételezi, hogy ismert a rejtett és a tippelt szó, az eddigi próbálkozások száma, és előállítja a tipp helyén a választ, növeli a próbálkozások számát feltéve, hogy a játékos nem akarta abbahagyni a játékot. Egy logikai változó jelezze a játék végét, amely lehet sikeres, lehet sikertelen. A = ( rejt, tipp : String, darab : N, ki : ) Ef = ( rejt=rejt tipp=tipp darab=darab ) Uf = ( rejt=rejt darab=darab

118 ki = (tipp = rejt tipp = x ) ki tipp = rejt i [1.. rejt ]: (tipp [i] rejt[i] tipp[i]=. ) (tipp [i]=rejt[i] tipp[i]= tipp [i]) ) Ezt a résztevékenységet egy ciklusba kell majd szervezni, ami addig tart, amíg a ki változó nem lesz igaz. Majd ezután megfelelő üzenetet írunk ki attól függően, hogy a játékos eltalálta a rejtett szót vagy feladta a játékot. Absztrakt program A résztevékenységet megoldó programot annak utófeltétele alapján könnyen elkészíthetjük. darab:=darab+1 ki := (tipp = rejt tipp = x ) ki tipp := rejt i = 1.. rejt SKIP tipp[i] tipp[i] :=. rejt[i] SKIP Az absztrakt programban a tipp := rejt értékadást úgy kell megvalósítani, hogy ha a tipp hosszabb, mint a rejt, akkor levágjuk a felesleges betűket a végéről, ha rövidebb, akkor kiegészítjük annyi darab ponttal, hogy a hossza megfelelő legyen. Implementálás A programkódban a klasszikus hármas (bevezetés, számolás, befejezés) tagolás két szinten jelenik meg. Belső szintje a specifikációban részletezett részfeladat megoldása, amelyik az absztrakt program kódjából és az azt 118

119 megelőző beolvasásából (az aktuális tippet vagy a kilépési szándékot kell itt megkérdeznünk) valamint az azt követő (a próbálkozás eredményének) kiírásból áll. Ezt ágyazzuk be a külső szintbe, amely kezdeti értékadásokkal kezdődik (a rejtvény bekérése, a próbálkozások darabszámának lenullázása, a kilépést jelző logikai változó hamisra állítása), majd a belső szint ciklikusan ismételt végrehajtása következik, végül az eredmény kiírására kerül sor. Belső szint implementálása Az absztrakt program kódolása önmagában nem jelenthet gondot. A vezérlési szerkezetek kódolását ugyanis már ismerjük, legfeljebb a karakterláncok (sztringek) használatával kell megbirkóznunk. Egy karakterlánc hasonlít egy karaktereket tartalmazó tömbre, amennyiben egy sztring i-edik karakterére a tömböknél látott indexeléssel hivatkozhatunk. A tipp kiértékelését végző ciklus kódolásához szükség van a karakterlánc hosszának ismeretére (size()). for(int i=0; i<(int)rejt.size(); ++i) if(tipp[i]!= rejt[i]) tipp[i] = '.'; A tipp := rejt értékadás megvalósításához egyfelől le kell vágnunk a tipp hosszából, ha az túlnyúlna a rejtvény hosszán, másfelől meg kell nyújtani, ha rövidebb lenne, mint a rejtvény. Egy sztringhez hozzáfűzhető egy másik sztring, kereshetünk benne adott részsztringet vagy karaktert, kivághatunk belőle egy részt. A substr() függvény segítségével egy karakterláncnak egy részét, megadott pozíciótól kezdődő megadott darabszámú karakterét tudjuk kivágni. A tipp.substr(0,n) a tipp karakterláncnak n hosszú elejét (nullával kezdődik egy sztring indexelése) kapjuk meg. Ha az n hosszabb, mint a tipp hossza, akkor a visszakapott rész-sztring maga a tipp lesz. tipp = tipp.substr(0, (int)rejt.size()); Egy sztringhez a + operátorral fűzhetünk hozzá másik sztringet. Például str+"bővítmény". Az str=str+"bővítmény" értékadást 119

120 str+="bővítmény" alakban rövidíthetjük. Egyformán működik az str=str+"y" és az str=str+ y, mert a sztringhez a + operátorral nemcsak sztringeket, hanem karaktereket is hozzá lehet fűzni. Általában azonban meg kell különböztetnünk az egyetlen karakterből álló karakterláncot a karaktertől. Ügyeljünk arra, hogy a sztringeket idézőjelek közé, a karaktereket aposztrófok közé kell írni, és ne lepődjünk meg, hogy az y =="y" vizsgálat szintaktikailag helytelen. A nyújtás során olyan karaktereket (például pontokat) kell hozzáírnunk a tipphez, amelyek biztosan nem egyeznek meg a rejtvénybeli adott pozíciójú karakterekkel hiszen azokat mivel egyáltalán nem szerepelt a tippünk ezen pozícióin karakter nem találtuk el a rejtvényből. for(int i=(int)tipp.size(); i<(int)rejt.size();++i) tipp += '.'; Nézzük most meg egyben a belső szint kódját. // Játékos szándéka cout << "\nha kilép, írjon be egy x-et. " << "Ha nem, tippeljen!\n"; cout << "Tipp = "; cin >> tipp; if(tipp!= "x") ++darab; ki = tipp == rejt "x" == tipp; if(!ki){ // tipp := rejt tipp = tipp.substr(0, (int)rejt.size()); 120

121 for(int i=(int)tipp.size(); i<(int)rejt.size(); ++i) tipp += '.'; // tipp kiértékelése for(int i=0; i<(int)rejt.size(); ++i) if(tipp[i]!= rejt[i]) tipp[i] = '.'; cout << endl << darab << ". próbálkozás = " << tipp << endl; Az absztrakt program kódját el kell látni megfelelő beolvasás és kiírás részekkel is. Az előfeltételben szereplő bemenő változók közül itt az absztrakt program előtt csak a játékos soron következő tippjének beolvasását kell elvégezni, a rejtvényt csak a legelső próbálkozás előtt, a külső szinten olvassuk be, és ugyanott nullázzuk le a próbálkozások darabszámát is, amely minden menetben eggyel nő. A tipp kiértékelése után pedig ha nem akart a játékos kilépni a játékból megjelenítjük, hogy hányadik próbálkozásnál járunk, és kiírjuk a kiértékelt tippet. Külső szint implementálása A belső szint kódját egyszer mindenképpen, de többnyire sokszor egymás után kell végre hajtani. Ezért ezt egy ciklusba ágyazzuk, ráadásul úgy, hogy annak a magja egyszer biztosan végrehajtódjon. Sok programozási nyelvben található olyan ciklusutasítás, amely éppen így működik; ezt hívják hátul tesztelő ciklusnak. A hátul tesztelős ciklust C++ nyelvben a do-while utasítással írhatjuk le. A do{ciklusmagwhile(feltétel) az alábbi programszerkezetnek felel 121

122 meg. A do-while utasítás alkalmazásával a ciklusmagot csak egyszer kell kódolni. ciklusmag feltétel ciklusmag E ciklus előtt azonban ne feledkezzünk meg a rejtvény beolvasásáról és a próbálkozások darabszámának nullára állításáról, utána pedig az eredmény megjelenítéséről. cout << "Adja meg a rejtett szót: "; cin >> rejt; int darab = 0; do{ // Belső szint while(!ki); if(tipp == rejt) cout << "\ngratulálok! " << darab << ". tippel nyert!\n"; else if ("x" == tipp) cout << "\nön vesztett! Majd legközelebb.\n"; 122

123 123

124 Tesztelés A fekete doboz teszteseteket két csoportba soroljuk. Egyrészt teszteljük a specifikációban részletezett részfeladat megoldó programját, másrészt azt a program környezetet, amelyik ezt a részt újra és újra lefuttatja, előtte inicializálja a játékot, a végén pedig kiírja az eredményt. Érvénytelen tesztesetek nincsenek. Az első csoport tesztesetei: 1. Olyan tipp, amelyik egyáltalán nem illeszkedik a rejt -hez. 2. Olyan tipp, amelyik csak a már ismert karaktereknél illeszkedik a rejt-hez. 3. Olyan tipp, amelyik a már ismert karaktereknél sem mindig illeszkedik a rejt-hez. 4. Olyan tipp, amelyik egyetlen újabb karaktert talál el rejt-ből. 5. Olyan tipp, amelyik több újabb karaktert talál el rejt-ből. 6. Olyan tipp, amelyik minden karaktert eltalál a rejt-ből. 7. Üres rejt vagy tipp karakterlánc esetei. 8. Eltérő hosszú rejt vagy tipp karakterlánc esetei. 9. Próbálkozások darabszáma növelésének ellenőrzése. A második csoport tesztesetei: 1. Sikeres eredmény kijelzése egy, kettő, több próbálkozásból. 2. Sikertelen eredmény kijelzése kilépés esetén egy, kettő, több próbálkozásból. 3. Próbálkozások darabszáma kezdeti értékének és végeredményének ellenőrzése, ha csak egy, kettő, több próbálkozás volt. A fehér doboz tesztelés a fenti tesztesetek lefedik, így újabb tesztesetekre nincs szükség. 124

125 Teljes program Végül nézzük meg teljes egészében a programkódot. #include <iostream> #include <string> using namespace std; int main() { // Rejtvény beolvasása string rejt; cout << "Adja meg a rejtett szót: "; cin >> rejt; int darab = 0; // Próbálkozások a rejtvény kitalálására string tipp; bool ki; do{ // Játékos szándéka cout << "\nha kilép, írjon be egy x-et. " << "Ha nem, tippeljen!\n"; cout << "Tipp = "; cin >> tipp; if(tipp!= "x") ++darab; ki = tipp == rejt "x" == tipp; 125

126 if(!ki){ // tipp := rejt tipp = tipp.substr(0, (int)rejt.size()); for(int i=(int)tipp.size(); i<(int)rejt.size(); ++i) tipp += '.'; // tipp kiértékelése for(int i=0; i<(int)rejt.size(); ++i) if(tipp[i]!= rejt[i]) tipp[i] = '.'; cout << endl << darab << ". próbálkozás = " << tipp << endl; while(!ki); // Eredmény kiírása if(tipp == rejt) cout << "\ngratulálok! " << darab << ". tippel nyert!\n"; else if ("x" == tipp) cout << "\nön vesztett! Majd legközelebb.\n"; return 0; 126

127 C++ kislexikon vektor definiálása konstans konstans méretű rögzített méretű vector<> const Element v[] = {,, ; const int size = sizeof(v)/sizeof(v[0]); const int maxsize = 100; Element v[maxsize]; int size; // size értéke: 0 size maxsize int size; // size értéke: size 0 Element v[size]; //NEM C++ #include <vector> int size; // size értéke: size 0 vector<element> v(size); vektor feltöltése for(int i=0; i<size; ++i){ cin >> v[i]; mátrix definiálása konstans méretű const int maxi = 100, maxj = 100; Element a[maxi][maxj]; int sizei, sizej; 127

128 // sizei értéke: 0 sizei maxi // sizej értéke: 0 sizej maxj rögzített méretű int sizei, sizej; // sizei értéke: 0 sizei // sizej értéke: 0 sizej Element a[sizei][sizej]; //NEM C++ vector<> (téglalap mátrix) #include <vector> int sizei, sizej; // sizei értéke: 0 sizei // sizej értéke: 0 sizej vector<vector<element>> a(sizei) for(int i=0,i<sizei,++i) t.resize(sizej); mátrix feltöltése for(int i=0; i<sizei; ++i){ for(int j=0; i<sizej; ++j){ cin >> a[i][j]; 128

129 4. Konzolos be- és kimenet Absztrakt programjaink tervezésekor feltételezzük, hogy a bemenő adatok értékeit a szükséges ellenőrzések után már az úgynevezett bemenő változók tartalmazzák, az eredmény pedig az eredmény változókba kerül. Egy futtatható program azonban nem állhat kizárólag az absztrakt program kódjából, hiszen olyan részeket is tartalmaznia kell, amelyek a bemenő adatokat a felhasználótól a bemenő változókba juttatják el, közben elvégzik az ellenőrzéseket, és az eredményt a felhasználó számára is érthető (olvasható) formában jelenítik meg. Annak, hogy a felhasználó adatokat adjon meg egy programnak és adatokat kapjon tőle, több módja is van. Lényegesen korlátozza a lehetőségeinket az a tény, hogy ebben a könyvben nem grafikus felhasználói felületet használunk begépelt adatok beolvasásához illetve az eredmény kiírásához, hanem vagy konzolablakos technikát, vagy szöveges állományokat. (Ez utóbbival a következő fejezetben foglalkozunk.) Ez egy szöveges alapú kommunikáció a felhasználó és a program között. Legkisebb egységei a karakterek, amelyeket sorban egymás után lehet csak beírni vagy megjeleníteni, és nincs lehetőség egy korábban be- vagy kiírt karakterhez visszatérni, azt módosítani. Implementációs stratégia Egy alkalmazás be- és kimeneti (input-output) tevékenysége nem kizárólag a bemenő adatok beolvasásából és az eredmény adatok kiírásából áll. Nagyon fontos, hogy ez olyan körítésben, olyan tájékoztató üzentekkel együtt történjen meg, amelyből a felhasználó (nem a programozó) tudni fogja, hogy mikor mit kell csinálnia, mit vár tőle a program, mit kaphat ő a programtól. A felhasználóbarát szoftver fontos ismérve ez a fajta öndokumentáltság, azaz az a képesség, hogy a program használata annak működése közben érthetővé, megtanulhatóvá váljon. Ennek kialakítása nem implementációs kérdés, hanem az ember-gép kapcsolat megértésének és tervezésének, a szoftverergonómiának a része, amelyet az implementáció keretében kódolunk hozzá az alkalmazáshoz. Ez többek között meghatározza, hogy az adatokat és üzeneteket milyen formában, milyen adagokban közvetítse a 129

130 program a felhasználó felé, illetve az adatokat hogyan kérje be a felhasználótól. Az input-output tevékenység másik velejárója a bemenő adatok ellenőrzésének elvégzése, az úgynevezett bolond-biztos alkalmazás készítése. Ez azt jelenti, hogy a felhasználó se hozzá nem értésből, se rosszindulatból ne tudjon olyan adatokat megadni a programnak, amitől az nem várt működést végez: például váratlan leáll (abortál) vagy végtelen ciklusba esik. Azt, hogy milyen adatokat vár a program azt a specifikáció megmutatja. Az input-output tevékenységért felelős kódba olyan ellenőrzéseket kell beépíteni, amelyek eleve megakadályozzák a nem kívánatos adatokkal való számításokat. A beolvasásnál alkalmazott adatellenőrzésnek több szintje is van. A legkülső szint a beolvasás szintje. Már ekkor is bekövetkezhet ugyanis hiba, még mielőtt módunkban állna a beolvasott értéket megvizsgálni. Ahhoz ugyanis, hogy egy adat értékét le tudjunk ellenőrizni, annak egy változóba kell bekerülnie. Ha például egy számot tartalmazó változóba egy nem számformájú adatot olvasunk be, akkor a program a legtöbb nyelven abortál. Ezt el tudjuk úgy elkerülni, ha az adatot először egy általános, bármilyen karakterláncot befogadni tudó változóba (mondjuk egy sztringbe) olvassuk be, majd ezután megvizsgáljuk, hogy az a várt formájú karakterekből áll-e. Ha nem, jelezzük a hibát a felhasználónak. 1. Beolvasott érték formátumának ellenőrzése 2. Beolvasott érték és az azt fogadó bemenő változó típusának (az állapottérnek) egyeztetése 3. A bemenő változókra megfogalmazott előfeltétel vizsgálata 5-1. ábra. Adatellenőrzés szintjei A második szint az adatoknak a feladat által elvárt feltételekkel való egybevetése. Ez egyfelől az állapottérrel való megfelelést, másfelől az 130

131 előfeltétel vizsgálatát jelenti. Előbbire példa az, amikor egy bemenő változó a feladat specifikációja szerint természetes szám, de az adott programozási nyelv ezt csak egy integer (egész típusú) változóba képes beolvasni. Ilyenkor ellenőrizni kell, hogy a beolvasott érték nem negatív-e, azaz tényleg természetes szám-e. Ezt követően vizsgálni kell, hogy a beolvasott érték kielégíti-e az előfeltételben vele szemben támasztott követelményeket. Természetesen az ellenőrzés különböző szintjeit nem kell a kódban szétválasztani, azok összevonhatók. Érdemes figyelni arra, ha több adat bekérésére is sor kerül egymás után, akkor minden beolvasás után különkülön végezzünk ellenőrzést. Így könnyebb a felhasználót a hiba okáról tájékoztatni. Fontos kérdés, hogy mit tegyünk akkor, ha hibát észlelünk az ellenőrzés során. Alapvetően kétféle stratégia létezik. Az egyik az, hogy hiba észlelése esetén pánikszerűen kilépünk az alkalmazásból. A másik módszer újra bekéri a hibás adatot és ismét ellenőrzi. Az elsőnek hátránya az, hogy ha már jó néhány adatbekérésen túl vagyunk, akkor a kilépés miatt a felhasználó korábban megadott helyes adatai is mind elvesznek. A második megoldás akkor kényelmetlen, ha nehéz kitalálnia a felhasználónak, hogy mit is rontott el; újra és újra megadja a kért adatot, de mindig rosszul. Nyilván lehet kombinálni a két módszert: hiba esetén történő ismételt adatbekérés esetén a felhasználó választhatja a kilépést. 1. Figyelmeztető üzenet után a program leállítása 2. Figyelmeztető üzenet után az adat ismételt bekérése 4-2. ábra. Hibás adat lekezelésének technikái Tovább fokozható a szoftver használatának kényelme az alkalmazás végtelenítésével, amikor is a program befejeződésekor megkérdezzük a felhasználót, hogy akar-e újabb futtatást végezni. Így nem kell újraindítani az alkalmazást, csak a végén az Igen. Folytatom. választ megadni. Ennek továbbfejlesztéseként is felfogható az, amikor egy futtatási ciklusban 131

132 nemcsak arról dönthet a felhasználó, hogy akarja-e folyatatni a futtatást, hanem arról is, hogy azt milyen feltételek mellett akarja elvégezni, esetleg egy több funkciós programnál kiválaszthatja, melyik funkciót akarja kipróbálni. Itt egy úgynevezett menü implementálására van szükség. Ha az egész alkalmazásra vonatkozik a menü, amely ráadásul végtelenítve van, akkor mindig szerepeljen a menüpontok között a befejezést választó eset. Még az eredetileg helyes absztrakt programot is félre lehet kódolni, ezért utólag azt is ellenőrizni, tesztelni kell, de az input-output tevékenységért felelős kód megfelelő működését csak így lehet ellenőrizni, hiszen a tervezés nem tér ki ennek részletezésére. Nyelvi elemek A futtatási környezetek mindig biztosítanak valamilyen karakteres üzemmódú soros elérésű konzolablakos input-output lehetőséget. Ennek használata során a program különféle típusú értékeket vár a felhasználótól, és küld a felhasználó felé. Bevitelnél az értékeket rendszerint a billentyűzet segítségével gépelhetjük be úgy, hogy az értéket kifejező karaktersorozatot adjuk meg. Beviteli tevékenységünket az <enter> billentyű megnyomásával fejezzük be. A begépelt karakterek a konzolablakban is megjelennek az aktuális pozíción. A kiírt értékek az őket leíró karaktersorozat formájában jelennek meg a konzolablakban. A konzolablakba történő íráskor adott egy aktuális sor és azon belül egy aktuális pozíció, amely azt mutatja, hogy a kiírandó következő karaktert hol kell megjeleníteni. Egy karakter kiírása után az aktuális pozíció a soron következő pozíció lesz, sor vége esetén ez a következő sor eleje. cin >> változó cout << kifejezés 132

133 4-3. ábra. Szabványos input/output Korszerű, több programozási nyelv által használt input-output technika, az, amelyik a beolvasáshoz egy bemenő-, a kiíráshoz egy kimenő szöveges adatfolyamot használ. C++ nyelvben a cin egy a standard névtérben előre definiált olyan speciális bemeneti szöveges adatfolyam, amely a szabványos bemeneten (ez többnyire a billentyűzet) keletkező karaktersorozatot megkapja, és amelyből aztán különböző értékeket tudunk kiolvasni. Ennek fordítottjaként a cout adatfolyamba különféle értékeket tudunk beírni karaktersorozat formában, amely végső soron a szabványos kimenetre (többnyire a konzol ablakba) kerül. A cin >> változó olvasó utasítás az adatfolyam karakterei alapján próbálja a változó értékét előállítani. Ha az adatfolyam üres, akkor vár legalább egy karakter, majd <enter> billentyű leütésére. A beolvasás az adatfolyamban levő karakterek sorozatát dolgozza fel. Alapértelmezett beállításkor először elhagyja a sorozat elején álló elválasztó jeleket (szóköz, tabulátor jel, sorvége jel). Ezután sorban kiveszi azokat a karaktereket, amelyekből a változó számára értéket tud előállítani. Például, ha a változó egész típusú, akkor az első karakter lehet számjegy vagy egy előjel (+/-), az azt követő karakterek pedig számjegyek. A kiolvasott karaktersorozat értékét a változó kapja meg. Lehet, hogy ezek után már nem marad több karakter az adatfolyamban, azaz az olvasás után az adatfolyam kiürül, de lehet, hogy olyan karakter következik, amely már nem értelmezhető a változó értékeként, akkor ez az adatfolyamban marad. Hibás működést okoz, ha az olvasás nem tud értéket előállítani az adatfolyamban levő karakterekből, például ha egy a betűt gépelünk be egy egész típusú változó beolvasásához. Egy utasítással egyszerre, pontosabban egymás után több változónak is értéket lehet adni: cin >> változó1 >> változó2. Egy adat beolvasását célszerű összekapcsolni az adat ellenőrzésével. Erre kétféle technikát említettünk. Hibás adat esetén figyelmeztetést írunk ki, majd leállítjuk a program futását, vagy újra és újra megpróbálkozunk az adatbekéréssel. Az előbbihez megfelelő nyelvi elem az elágazás, az utóbbihoz 133

134 célszerű az úgynevezett hátul tesztelő ciklust használni, amennyiben ezt a nyelv biztosítja. Ilyenkor ugyanis az olvasási és ellenőrzési folyamatot egyszer mindenképpen el kell végezni, és csak az ellenőrzés után derül ki, hogy meg kell-e ismételni ezeket a tevékenységeket, vagy tovább léphetünk. 134

135 Adatfolyamok Az adatfolyamok (stream) olyan objektumok, amelyek a hozzájuk eljuttatott adatokat a beírásuk sorrendjében adják vissza, azaz sor (FIFO) módjára működnek. Mivel az adatokat valamilyen kódsorozat formájában tárolja, ezért az adatfolyam képes arra is, hogy ezt a sorozatot a beírás logikájától eltérő szeletekre bontsa, és az egyes szeleteket kívánt értékké alakítva adja vissza. Leggyakrabban a szöveges adatfolyamokkal találkozhatunk, amelyek között megkülönböztetünk bemeneti- illetve kimeneti adatfolyamot. A bemeneti szöveges adatfolyamnak egy sztringet adunk inputként, amelynek karaktereit a kiolvasás által meghatározott értékekké képes átalakítani és visszaadni. Ezt úgy éri el, hogy a sztringet megfelelő szeletekre vágja, és az egyes szeleteket alakítja át adott típusú értékké. A kimeneti szöveges adatfolyamnak különböző értékeket lehet egymás után megadni, amelyeket karakterláncokká alakít át és egyetlen sztringbe fűz össze, amelyet aztán eredményként visszaad. Szöveges adatfolyamokkal kényelmesen végezhetők el különféle átalakítások (konverziók) sztringek és egyéb értékek között. A konzolos illetve szöveges állományból történő beolvasáshoz bemeneti-, a kiíráshoz kimeneti speciális szövegfolyamokat használunk. Ezeknek az adatfolyamoknak vagy a bemenete, vagy a kimenete valamilyen perifériához (billentyűzet, képernyő, szöveges fájl) kapcsolódik. Bemenet lehet például a billentyűzeten keletkező karaktersorozat, kimenet a konzolablakra szánt karakterek sorozata, de lehet más, például a merevlemezen tárolt, sorosan olvasható vagy írható állomány is. Ennek a technikának az előnye az, hogy függetleníti a programot a perifériáktól, egységessé teszi az input-output tevékenységet, nem nagyon kell különbséget tenni például a billentyűzetről vagy egy szöveges állományból való beolvasást végző kód között. A hátul tesztelő ciklus a ciklusmagot egyszer mindenképpen végrehajtja, és csak azt követően ellenőrzi a ciklus feltételt. Tulajdonképpen egy olyan szekvencia, amelyiknek első tagja a ciklusmag, második tagja pedig 135

136 egy szokásos (elöl tesztelő) ciklus, amelyiknek a ciklus magja azonos a szekvencia első tagjával. A C++ nyelvben a hátul tesztelős ciklust a do-while utasítással adhatjuk meg. Fontos megemlíteni, hogy a while(ciklusfeltétel) részben a zárójelezett kifejezés azt a logikai értéket állítja elő, amelynek igaz értéke esetén a do és while közötti utasítást (vagy utasítás blokkot) meg kell ismételni, hamis értéke esetén a ciklus utáni utasításra kerül a vezérlés. (Ez éppen a fordítottja a Pascal nyelvbeli repeat-until utasítás működésének.) ciklusmag do{ ciklusfeltétel ciklusmag ciklusmag while(ciklusfeltétel) 4-4. ábra. Hátul tesztelő ciklus Ugyancsak hátul tesztelő ciklussal érdemes egy alkalmazás végtelenítését kódolni. (Erre a 8. feladatnál már láttunk példát.) C++ nyelven ezt általában úgy érhetjük el, hogy a main függvény törzsét az utolsó return 0 utasítás kivételével egy do-while utasításba ágyazzuk, és a ciklusmag végén megkérdezzük a felhasználót, akarja-e megismételni a program futását. int main() { char ch; do{... cout << "Folytatja? (I/N): "; cin >> ch; 136

137 while(ch!= 'n' && ch!= 'N'); return 0; Ennek egy továbbfejlesztett változata az, amikor egy menü segítségével vezéreljük a program futását. Erre a 10. feladat majd mutat példát. A fenti eseteken kívül a do-while ciklus alkalmazását programjainkban nem ajánljuk. A C++ nyelvben a kiírás a cout << kifejezés vagy cout << kifejezés1 << kifejezés2 utasításokkal végezhető. A kiírás a konzolablak azon pontján kezdődik, ahol az előző kiírás befejeződött. Amikor egy sor betelik, a kiírás automatikusan a következő sorban folytatódik. A kiírás során szívesen élünk olyan lehetőségekkel, amellyekkel a kiírt értékeket felhasználóbarát formában tudjuk elhelyezni a konzolablakban. Már egy szóköz kiírásával jól szeparálhatóak a megjelenített adatok, de használhatunk pozícionált kiírást. A cout << setw(10)<< kifejezés például a következő 10 pozíción helyezi el a kifejezés értékét, attól függően balra vagy jobbra tömörítve, hogy milyen típusú a kiírt érték, illetve hogyan állítottuk be ezt az opciót előzőleg. (Erről illetve az ehhez hasonló opciókról a következő bekezdésben írunk részletesen.) A tabulálást a cout << \t kiírás, a soremelést a cout << endl vagy cout << \n biztosítja. Végezetül ki kell térnünk a beolvasással kapcsolatos igen kellemetlen jelenségre. Ha például egy egész számot akarunk beolvasni az eddig alkalmazott int n; cin >> n; technikával, de nem számformájú karaktereket gépelünk be, akkor a beolvasás elromlik. Az nem meglepő, hogy az n változó ilyenkor nem kap értéket, de az már igen, hogy az ezt követő összes beolvasás sem fog már helyesen működni. 137

138 Ezt a jelenséget kétféleképpen tudjuk kezelni. Az egyik lehetőség az, hogy egy beolvasást követően rákérdezünk arra, hogy a beolvasásnak milyen az állapota. A cin.fail() függvény hamis értéket ad vissza, ha a beolvasás elromlott. Ebben az esetben hibaüzenetet küldhetünk a felhasználónak, újra bekérhetjük az adatot. Ahhoz azonban, hogy az ezt követő beolvasások sikerüljenek, ki kell törölnünk a beolvasás emlékeit. Egyrészt visszaállítjuk a beolvasás állapotát úgy, hogy a fail() függvény ne jelezzen továbbra is hibát (cin.clear()), másrészt kiürítjük (getline(cin,tmp)) a beolvasás során bevitt, de fel nem dolgozott karaktereket a bemeneti adatfolyamból. Fontos, hogy ezt a kiürítést megelőzze a cin.clear() utasítás. Manipulátorok, formátumjelzők C++ nyelvben A formázott kimenetet a manipulátorok és a formátumjelző bitek (flag) segítségével vezérelhetjük. A formátumjelző bitek a kiírás formáját határozzák meg. Ilyen például a scientific (lebegőpontos alak), fixed (fixpontos alak alapértelmezett), right, left (jobbra ill. balra tömörítés), dec, hex, oct (számrendszer, dec az alapértelmezett), showpoint (tizedespont mindig látszódjon), showpos (pozitív előjel is látszódjon), uppercase (csupa nagybetű), boolalpha (logikai érték kiírásához). Ezeket a tulajdonságokat a setf() illetve unsetf() függvények segítségével kapcsolhatjuk be illetve ki. E függvények argumentumában jellel elválasztva sorolhatjuk fel a szükséges formátumjelző biteket. Némelyik tulajdonság alapértelmezett módon be van kapcsolva. Minden formátumjelző bit elé az ios:: minősítést kell írni. Ha például a számokat lebegőpontos alakban, balra tömörítve szeretnénk kiírni úgy, hogy a tizedespont és az előjel minden esetben látszódjon, akkor ezt a cout.setf(ios::scientific ios::showpoint ios::showpos) utasítással állíthatjuk be. A manipulátorokat a << operátorral kell az adatfolyamra elküldeni, és a következő kiírásra van hatással. Az endl kiírás egy soremelést eredményez, a setw(int w)-vel a következő kiírás számára fenntartott szélességet, pozíció számot adhatjuk meg. Egy valós szám törtjegyeinek számát például a setprecision(int p), kitöltő karaktereket a setfill(char c) segítségével 138

139 idézhetjük elő. A paraméterrel rendelkező manipulátorok eléréséhez az iomanip csomagra van szükségünk. Számos formátumjelző bitként megadható tulajdonság manipulátorként is be- illetve kikapcsolható (showpos / noshowpos). Ez a technika nemcsak a kiíráshoz használt I/O adatfolyamra alkalmazható. Formátumjelző bitekkel és manipulátorokkal szabályozható a beolvasás is. Természetesen más tulajdonságok állíthatóak be a beolvasásnál, mint a kiírásnál. Csak beolvasásnál van értelme például a vezető üres karakterek átugrására (cin >> ws >> ), csak kiírásnál a csupa nagybetűs szövegként való megjelenítésre (cout << uppercase << ), a logikai értékek kezelését előíró tulajdonság (boolalpha) viszont beolvasásnál is, kiírásnál is használható. int n; cin >> n; if(cin.fail()){ cout << "Hiba!\n"; cin.clear(); string tmp; getline(cin,tmp); Nemcsak a cin objektumra lehet ilyen hasznos függvényeket (fail(), clear(), getline()) meghívni, hanem a cout objektumra is. A cout objektumra hívható függvényekkel a kiírás formáját tudjuk befolyásolni. A konkrét lehetőségeket az alábbi alkalmazásokban mutatjuk majd be. A másik megoldás az, ha az adatbevitelre szánt karaktereket először mindig egy sztring típusú változóba olvassuk be, mert ez nem okozhat hibát, majd ellenőrizzük, hogy a sztringbe bekerült karakterek megfelelnek-e, például valóban egy egész számot írnak-e le. Ehhez felhasználjhatuk a C 139

140 nyelvtől örökölt atoi() függvényt, amelyik helyes formátum mellett a sztringből kiszámolt egész szám értékét, hibás formátum esetén a nullát adja vissza. (Valós számok beolvasásánál a lebegőpontos formátumot a C nyelvből örökölt atof() függvénnyel ellenőrizhetjük.) Így ha csak nem a 0 karaktert adjuk meg egész számként a nulla visszatérési érték a hibás adatformátumot jelzi. Sajnos az atoi() függvény csak régi (C stílusú) karakterláncokra működik, ezért át kell alakítanunk a beolvasott sztringünket ilyen lánccá a c_str() függvény segítségével. string str; cin >> str; int n = atoi(str.c_str()); if (0 == n && str!= "0"){ cout << "Hiba!\n"; 140

141 Különféle karakterláncok A C programozási nyelv a karakterláncokat olyan byte sorozatként ábrázolja a memóriában, amelynek a végét egy speciális jel, a \0 karakter kódja jelez. Az ilyen karakterláncok kezelése nagy körültekintést igényel. Nem is hinnénk milyen könnyen elhagyható vagy felülírható a legutolsó speciális karakter, és hogy ennek milyen komoly következményei lehetnek. Talán éppen ezért jelent meg egy igen gazdag beépített függvény készlet a karakterláncok kezelésére. Jóval biztonságosabb, de ugyanakkor korlátozottabb a Pascal programozási nyelvben található karakterlánc ábrázolás, amely egy rögzített méretű tömbben helyezi el a karakterláncot, külön tárolja annak hosszát, amely természetesen nem lépheti át a maximális méretet. A C++ nyelv a fenti két megoldást elegyítve bevezette a string típust, amely alapvetően a Pascal-os megoldásra hasonlít (nincs a karakterlánc méretének előre rögzített felső határa), de lehetőség van a C stílusú láncok mintájára bevezetett függvények használatára is. A C++ nyelv átvett olyan függvényeket is a C nyelvtől, amelyek argumentumának C stílusú karakterláncot kell megadni. (Egyébként C stílusú karakterláncokat is használhatunk C++ nyelven.) Ha ezeket a függvényeket (például a karakterláncot egész számmá konvertáló atoi()-t) szeretnénk használni egy C++ stílusú karakterláncra, azaz sztringre, akkor át kell tudnunk alakítani azt C stílusú karakterlánccá. Erre szolgál a c_str() függvény. Valójában ez egy körülményes megoldás arra a problémára, hogy a C++ nyelv nem jár el következetesen a karakterláncok használatakor. 141

142 9. Feladat: Duna vízállása Vízügyi szakemberek egy adott időszakban rögzítették a Duna Budapesten mért vízállásait. Készítsünk hisztogramot a mért adatokról, miután ellenőriztük az adatok helyességét. Ez a feladat különleges abból a szempontból, hogy tényleges számítást, tehát az előző fejezetek feladatainak megoldásainál előállított absztrakt megoldó programot nem igényel. A feladat ugyanakkor határozott kívánságot fogalmaz meg a kiírással szemben. Ezért az absztrakt program itt a korábbiaktól eltérő szerepkörben jelenik meg, a kiírás tervét hordozza. Tipikus határesettel van tehát dolgunk. Eddig a kiírás részekkel kizárólag az implementációban foglalkoztunk, de most ezt is részletesen specifikálni kell, részben absztrakt programot kell hozzá készíteni. Specifikáció A feladatnak csak bemenő változója van, ennek megfelelően célfeltétele sincs. A = ( v : Z n ) Ef = ( v=v i [1..n]: v[i] 0 ) A kiírás során egy hisztogramot kell megjelenítenünk. A hisztogram egy olyan diagram, amelynek alapvonalára merőlegesen egymás után annyi darab hasábot rajzolunk, ahány mérési adatunk van. A hasábok hossza a mért értékekkel arányos, rá is lehet írni minden hasábra a hozzátartozó értéket, a hasáb alá az alapvonalra pedig a mérési eseménnyel kapcsolatos információt (sorszámot, esetleg dátumot). A hisztogramot egy konzolablakban úgy kényelmes megjeleníteni, ha az alapvonalát függőlegesen, az ablak baloldalán képzeljük el. Így egy sor, a hisztogram egy hasábját tartalmazza, és ahány hasábból áll a hisztogram annyi sora a lesz a kiírásnak. (A szokásos elrendezés az, amikor a hisztogram alapvonala vízszintes. Ebben az esetben azonban a konzolablak szélessége korlátozná a kiírható hasábok számát.) Mivel erősen korlátozottak a lehetőségeink a hisztogram megjelenítésére, ezért az alapvonalat ne is 142

143 rajzoljuk ki, a hasábokat pedig megfelelő számú egymás után írt * karakterrel helyettesítsük. Írjuk ki a hasáb mellé, hogy az hányadik méréshez tartozik, és jelenítsük meg a mért értéket is. Absztrakt program A hisztogram arányos megjelenítéséhez szükség van a megjelenítendő legnagyobb értékre. A = ( v : Z n, max : Z ) Ef = ( v=v n>0 ) Uf = ( v=v max = max n i 1 v[i]) max := v[1] i = 2.. n v[i]>max max := v[i] SKIP Elég csak a maximális értéket kiszámolni, nincs szükség arra az indexre, amelyik a maximális elemet jelöli. Mivel a maximum kiválasztás csak akkor értelmes, ha van legalább egy mérés, ezért a fenti programot egy olyan elágazásba ágyazzuk, amelyik vizsgálja az n>0 feltételt. Ha ez nem teljesül, akkor nem rajzolunk hisztogramot. A maximális értékhez tartozó hasábot fogjuk a lehető leghosszabbra rajzolni, a többit pedig ehhez mérten arányosan jelenítjük meg. Magának a hisztogramnak a rajzolása annyiban tér el egy tömb elemeinek kiírásától, hogy egy tömbelem (index és érték) kiírását mindig új sorba tesszük, és ott megfelelő számú * karaktert is elhelyezünk. Ehhez ismernünk kell a konzolablak szélességét azaz azt az m értéket, amely azt jelöli, hogy legfeljebb mennyi csillag fér el a konzol ablakban vízszintesen: ennyi csillaggal fogjuk a legnagyobb hasábot megjeleníteni. A tömb egy 143

144 tetszőleges v[i] értékéhez tartozó hasábban (v[i]/max)*m egészrésze számú csillagot kell majd kiírnunk. i = 1..n i kiírása j = 1.. (v[i]/max)*m * kiírása v[i] kiírása Implementálás Beolvasás A vector<int> segítségével definiált tömb feltöltése lényegében megegyezik az előző fejezetben látottakkal, de most bolond-biztossá tesszük az egész számok beolvasását és hibás adatbevitel esetén egy hibaüzenet kiírása után azonnal leállítjuk a programot. Ezt alkalmazzuk a tömb elemszámának beolvasásánál és a tömb elemeinek beolvasásánál is. cout << "Adja meg a tömb hosszát (1 <= n )"; int n; cin >> n; if(cin.fail() n<=0){ cout << "Hiba!\n"; return 1; 144

145 vector<int> v(n); cout << "Adja meg a tomb elemeit!\n"; for(int i=0; i<n; ++i){ cout << i + 1 << ". elem: "; cin >> v[i]; if(cin.fail() v[i]<0){ cout << "Hiba!\n"; return 1; Kiírás Klasszikus értelemben vett számításrész most nincsen, viszont a kiírás módját az absztrakt program leírja. Ez egyrészt egy (az előző fejezetben már látott) maximum kiválasztásból áll, és egy formázott kiírást végző részből. int ind = 0, max = v[0]; for(int i=1; i<n; ++i){ if (v[i]>max){ ind = i; max = v[i]; int m = 30; 145

146 cout << endl; for(int i=0; i<n; i++){ cout << setw(5) << i+1 << " "; for(int j=0; j<(v[i]/max)*m; i++){ cout << setw(2) << "*"; cout << v[i] << endl; A soron következő formázott kiíráshoz használtuk a setw(int w) manipulátort, amely azt állítja be, hogy a soron következő kiírás w pozíción (helyen, szélességben) történjen. Szám kiírása esetén alapértelmezés szerint a megadott mező jobbszéléhez illesztve fog a szám értéke megjelenni, karakter, karakterlánc esetén a balszéléhez illesztve. Ennek a manipulátornak a használatához szükség van az iomanip könyvtárra. (#include <iomanip>). A manipulátorokat a << operátorral kell a kimeneti adatfolyamra elküldeni. A setw()-hez hasonló manipulátorral már korábban is találkoztunk: ilyen volt a soremelésre használt endl, csak ahhoz még nem kellett az iomanip könyvtár. Tesztelés Ez a program elsősorban fehérdoboz módszerrel tesztelhető. Négy szakaszra bontható: a tömb definiálása, tömb feltöltése, tömb maximális elemének kiválasztása és a kiírás. Az első három fázis tesztelése a 6. feladatáéval azonos módon történik. A kiírás rész tesztelése az eredmény megjelenési formájának ellenőrzéséből áll. Különböző hosszú és értékű tömb segítségével próbálhatjuk ki, hogy a kiírás egymásba ágyazott ciklusai megfelelően működnek-e. 1. Külső ciklus magja egyszer sem fut le. 146

147 2. Külső ciklus magja egyszer/többször fut le, a belső ciklus magja egyszer sem/egyszer/többször. 3. A tömbnek egy kiugróan magas értéke van. 147

148 Teljes program #include <iostream> #include <iomanip> #include <vector> #include <string> using namespace std; int main() { // Tömb definiálása cout << "Adja meg a tömb hosszát (1 <= n )"; int n; cin >> n; if(cin.fail() n<=0){ cout << "Hiba!\n"; return 1; vector<int> v(n); cout << "Adja meg a tomb elemeit!\n"; for(int i=0; i<n; ++i){ cout << i + 1 << ". elem: "; cin >> v[i]; if(cin.fail() v[i]<0){ cout << "Hiba!\n"; 148

149 return 1; // Maximumkeresés int ind = 0, max = v[0]; for(int i=1; i<n; ++i){ if (v[i]>max){ ind = i; max = v[i]; // Hisztogram rajzolása int m = 30; cout << endl; for(int i=0; i<n; i++){ cout << setw(5) << i+1; for(int j=0; j<(v[i]/max)*m; i++){ cout << " *"; cout << v[i] << endl; 149

150 10. Feladat: Alsóháromszög-mátrix Két, valós számokat tartalmazó alsóháromszög mátrixot (ez olyan mátrix, amelynek a főátlója feletti elemei mind nullák) szorozzunk össze! Miután a mátrixokat beolvastuk a billentyűzetről és elvégeztük az összeszorzásukat, egy menüből válasszuk ki, hogy milyen formában akarjuk a mátrixot megjeleníteni: kiírjuk-e a főátló feletti értékeket vagy csak a helyüket jelezzük; az értékek lebegőpontos formában vagy fixpontos formában jelenjen meg, illetve hogy a kiírás pontossága milyen legyen. Specifikáció A feladat két alsóháromszög mátrix összeszorzása. Az általános mátrixszorzás képlete: i [1..n]: j [1..n]: c[i,j] = ). Alsóháromszög mátrixok esetén ezt a képletet egyszerűsíteni lehet. Egyrészt az eredmény mátrixnak csak az alsóháromszög részét kell kiszámolnunk, mivel alsóháromszög mátrixok szorzata is alsóháromszög alakú. Másrészt a számolás során a biztosan nulla értékű szorzatokat (amikor valamelyik mátrixnak a jobb-felső területéről származó értékkel szorzunk) felesleges kiszámítanunk: i [1..n]: j [1..i]: c[i,j] = ) Érdemes azt is észrevenni, hogy egy alsóháromszög mátrix főátló feletti nulla értékeit nem kell külön eltárolni. Az alsóháromszög részbe eső elemek elférnek egy n(n+1)/2 elemű egydimenziós tömbben. Ha sorfolytonosan helyezzük ebbe a tömbbe a mátrix alsóháromszög részének elemeit, akkor az i-dik sor j-dik eleme a tömb i(i 1)/2+j -dik eleme lesz. Az állapottérbe az a, b, c : R n n mátrixok helyett a, b, c : R n(n+1)/2 vektorokat veszünk fel. Így nem kell az előfeltételben kikötni, hogy a felső háromszögrész elemei nullák, a beolvasásnál sem fogunk a felső háromszögrészre rákérdezni. A = (a, b, c : R n(n+1)/2 ) Ef = ( a=a b=b ) Uf = ( a=a b=b i [1..n]: j [1..i]: ( c[i(i 1)/2+j]= a[i(i 1)/2+k]* b[k(k 1)/2+j]) ) ) 150

151 Absztrakt program i = 1.. n j = 1.. i c[i(i 1)/2+j] := 0.0 k = j.. i c[i(i 1)/2+j] := c[i(i 1)/2+j] + a[i(i 1)/2+k] * b[k(k 1)/2+j] Implementálás Ügyeljünk arra, hogy a C++ nyelv 0-tól kezdődően indexeli a tömböket, ezért az absztrakt programban használt tömbindexelés mindenhol helyett eggyel csökkentett indexet használjunk. Például a mátrix i,j-edik elemére történő hivatkozáskor az i(i 1)/2+j helyett i(i 1)/2+j 1-et. Beolvasás Miután beolvastuk a mátrixok közös méretét (n), egy mátrix beolvasása, amely valójában egy egydimenziós tömb feltöltése, az alábbi szerint alakul: int n; cin >> n; vector<double> a(n*(n+1)/2); for(int i=1; i<=n; ++i){ for(int j=1; j<=i; ++j){ cout << "a[" << i << "," << j << "]= "; 151

152 cin >> a[(i-1)*i/2 + j - 1]; A végleges változatban azonban minden beolvasás köré építsünk egy hibaellenőrző hátul-tesztelős ciklust, amely mindaddig újra és újra adatot kér, amíg nem adjuk meg azt helyesen. Számolás A szorzás az absztrakt program hű másolata. Felhasználjuk azt, hogy a vector típusú adat létrehozásakor meg lehet adni egy a vektor elemeit kezdetben kitöltő értéket. Kiírás vector<double> c(n*(n+1)/2, 0.0); for(int i=1; i<=n; ++i) for(int j=1; j<=i; ++j) for(int k = j; k<=i; ++k) c[(i-1)*i/2+j-1]+= a[(i-1)*i/2+k-1]*b[(k-1)*k/2+j-1]; A mátrix kiírásához először megkérdezzük a felhasználót, hogy milyen formában szeretné látni az adatokat. Egy menü segítségével két lehetőséget kínálunk fel. Az első esetben a nulla értékű főátló felett elemeket is kiírjuk, és minden értéket fixpontos formában 2 tizedesjegy pontossággal, tíz pozíción balra igazítva. Az alábbi kódban megfigyelhető, hogy ehhez milyen manipulátorokat és formátumjelzőket használtunk fel a cout beállításához. 152

153 Az egyéb beállítási lehetőségeket a fejezet végén található C++ kislexikonban olvashatjuk. cout.setf(ios::fixed ios::left ios::showpoint); cout << setprecision(2); for(int i=1; i<=n; ++i){ for(int j=1; j<=n; ++j){ if(i>=j) cout << setw(10) << c[(i-1)*i/2+j-1]; else cout << setw(10) << 0.0; cout << endl; A második esetben a főátló feletti elemeknek csak a helye látszik, a nulla értékek nem, a többi szám pedig lebegőpontos formában 1 tizedesjegy pontossággal, mindig mutatva az előjelet, tíz pozíción jobbra igazítva. cout.setf(ios::scientific ios::showpos); cout << setprecision(1); for(int i=1; i<=n; ++i){ for(int j=1; j<=i; ++j){ cout << setw(10) << c[(i-1)*i/2+j-1]; cout << endl; 153

154 A fenti két programrészt az alábbi kódba ágyazzuk. char ch; do{ cout << "Kiírás módja:\n"; cout << "1 - Főátló feletti elemekkel, " << "fixpontosan, balra igazítva, " << "2 tizedesjegy pontossággal\n"; cout << "2 - Főátló feletti elemek nélkül, " << "lebegőpontosan, előjelesen, " << << "1 tizedesjegy pontossággal\n"; cout << "Válasszon: "; int k; cin >> k; switch(k){ case 1: // első fajta kiírás break; case 2: // második fajta kiírás break; default:; cout << "Folytatja? (I/N): "; cin >> ch; 154

155 while(ch!= 'n' && ch!= 'N'); Ez egy végtelenített működést valósít meg. Az ismétlődő rész egy kételemű menüt ajánl fel, és a megfelelő menüpont kiválasztásával a kiírás formáját lehet meghatározni. Ehhez a C++ nyelv speciális elágazó utasítását, a switch() utasítást használjuk. Az utasítással egy kifejezést adunk meg (itt ez egyszerűen a k), amelynek lehetséges értékeit sorolják fel az esetek (case). Minden esethez tartozó program végén a break utasítás áll, különben a vezérlés a következő eset programjára kerül, nem pedig a switch után. Ha a kifejezés olyan értéket vesz fel, amelyik nem szerepel esetként, akkor a switch utasítás nem csinál semmit. Ezt emeli ki az üres utasítású default ág, amelyre ugyan nincs szükség, de explicit módon való kiírása a biztonságos kód kialakítását támogatja. Tesztelés A feladat szempontjából készített fekete doboz tesztesetek: Érvényes tesztesetek: 1. Null-mátrixszal való (jobbról, balról) szorzás (Eredmény: nullmátrix) 2. Egység-mátrixszal való (jobbról, balról) szorzás (Eredmény: a másik mátrix) es mátrixok szorzása (0-val jobbról-balról, eredmény nulla; 1- gyel jobbról-balról, eredmény a másik szám; általános eset) es mátrixok szorzása (Null-mátrixszal jobbról-balról, eredmény: null-mátrix; egység-mátrixszal jobbról-balról, eredmény: a másik mátrix; általános eset) es mátrixok szorzása (lásd 2 2-es eseteket) es mátrixok szorzása (lásd 2 2-es eseteket) 7. A kommutativitás vizsgálata Érvénytelen tesztesetek: 155

156 1. Hibás méret (<=0) beírása. A programkód alapján készített tesztesetek: 1. Mátrix méretének beolvasása és ellenőrzése: amikor a ciklus egyszer fut le (jó adat), többször fut le (először rossz adat, a végén jó). 2. Mátrixok feltöltését és a mátrixok szorzását végző ciklusok ellenőrzése (fekete doboz tesztestek) 3. A kiírás tesztelése (kétféle kiírás ciklikusan ismételve) 156

157 Teljes program #include <iostream> #include <iomanip> #include <vector> #include <string> using namespace std; int main() { // Mátrix méretének beolvasása int n; bool error; do{ cout << "Adja meg a mátrixok méretét: "; cin >> n; if(error = cin.fail() n<1){ cout << "Pozitív egész szám legyen!\n"; cin.clear(); string tmp; getline(cin,tmp); while(error); 157

158 // Első mátrix beolvasása cout << "Első mátrix:\n"; vector<double> a(n*(n+1)/2); for(int i=1; i<=n; ++i){ for(int j=1; j<=i; ++j){ do{ cout << "a[" << i << "," << j << "]= "; cin >> a[(i-1)*i/2+j-1]; if(error = cin.fail()){ cout << "Valós szám legyen!\n"; cin.clear(); string tmp; getline(cin,tmp); while(error); // Második mátrix beolvasása 158

159 cout << "Második mátrix:\n"; vector<double> b(n*(n+1)/2); for(int i=1; i<=n; ++i){ for(int j=1; j<=i; ++j){ do{ cout << "b[" << i << "," << j << "]= "; cin >> b[(i-1)*i/2+j-1]; if(error = cin.fail()){ cout << "Valós szám legyen!\n"; cin.clear(); string tmp; getline(cin,tmp); while(error); // Mátrix-szorzás vector<double> c(n*(n+1)/2, 0.0); for(int i=1; i<=n; ++i) for(int j=1; j<=i; ++j) for(int k = j; k<=i; ++k) c[(i-1)*i/2+j-1]+= a[(i-1)*i/2+k-1]*b[(k-1)*k/2+j-1]; //Kiírás kétféleképpen char ch; 159

160 do{ cout << "Kiírás módja:\n"; cout << "1 - Főátló feletti elemekkel, " << "fixpontosan, balra igazítva, " << "2 tizedesjegy pontossággal\n"; cout << "2 - Főátló feletti elemek nélkül, " << "lebegőpontosan, előjelesen, " << << "1 tizedesjegy pontossággal\n"; cout << "Válasszon: "; int k; do{ cout << "Adja meg a mátrixok méretét: "; cin >> k; if(error = cin.fail() n<1){ cout << "Pozitív egész szám legyen!\n"; cin.clear(); 160

161 string tmp; getline(cin,tmp); while(error); switch(k){ case 1: cout.setf(ios::fixed ios::left ios::showpoint); cout << setprecision(2); for(int i=1; i<=n; ++i){ for(int j=1; j<=n; ++j){ if(i>=j) cout << setw(10) << c[(i-1)*i/2+j-1]; else cout << setw(10) << 0.0; cout << endl; break; case 2: cout.setf(ios::scientific ios::showpos); cout << setprecision(1); for(int i=1; i<=n; ++i){ for(int j=1; j<=i; ++j){ cout << setw(10) 161

162 << c[(i-1)*i/2+j-1]; cout << endl; break; default:; cout << "Folytatja? (I/N): "; cin >> ch; while(ch!= 'n' && ch!= 'N'); return 0; 162

163 C++ kislexikon standard beolvasás Standard kiírás cin >> változó; cin >> változó1 >> változó2; cout << kifejezés; cout << kifejezés1 << kifejezés2; természetes szám ellenőrzött beolvasása int n; cin >> n; if(cin.fail() n<0){ cout << "Hibás szám!\n"; exit(1); int n; bool error; do{ cin >> n; error = cin.fail() n<0; if(error){ cout << "Hibás szám!\n"; cin.clear(); 163

164 string tmp; getline(cin,str); while(error); végtelenített futtatás char ch; do{ cout << "Folytatja? (I/N): "; cin >> ch; while(ch!= 'n' && ch!= 'N'); menü char n; do{ cout << "Menü pontok jelentése "; cout << "Válassz: ";cin >> n; switch(n){ case 1: ; break; case 2: ; break; default: ; while(n!= 0); szerkesztett input-output #include <iomanip> 164

165 cin.setf(ios::flag) cin.unsetf(ios:: flag) cout.setf(ios:: flag) cout.unsetf(ios:: flag1 ios:: flag2) cout << manipulator formátum jelzők (flag) manipulátorok (manipulator) scientific, fixed right, left, dec, hex, oct, showpoint, showpos skipws boolalpha uppercase setw(int w) width(int w) setprecision(int p) precision(int p) lebegőpontos ill. fixpontos alak jobbra ill. balra tömörítés megjelenítés számrendszere tizedespont ill. előjel mindig látszódjon elválasztó jelek átlépése olvasáskor logikai érték kiírásához csupa nagybetű mezőszélesség megadása számábrázolás pontossága 165

166 setfill(char c) endl kitöltő karakter definiálása sorvége 166

167 5. Szöveges állományok A szöveges állományok (szöveges fájlok) az adatokat karakteres formában háttértárolókon tárolják. Tartalma egy szöveg, amely lényegében egyetlen, többnyire igen hosszú, a felhasználó által könnyen elolvasható karakterlánc. A karakterek között lehetnek olyanok is, amelyek a szöveg megjelenítését befolyásolják. Már egyszerű szövegszerkesztők is képesek egy ilyen szöveget úgy megjeleníteni, hogy a szöveg ilyen speciális karaktereit a megjelenítés formáját meghatározó jeleknek (tabulátor, sorvége) tekintik. A szöveges állományokat kétféle célból fogjuk használni: olvasásra illetve írásra. Ehhez az első esetben egy vedd a következő adatot jellegű olvasó műveletre, a második esetben tedd az eddigi adatok után írás műveletre lesz szükségünk. Implementációs stratégia Egy alkalmazás implementálásakor sokszor feltett kérdés az, hogy a bemenő adatokat billentyűzetről vagy szöveges állományból vegyük-e, illetve az eredményt terminálra vagy szöveges állományba írjuk-e. Egyszerűbb, kevesebb előkészítést igényel a konzolablakos input-output, ráadásul egy hibásan beírt adatot azonnal, a program futása közben lehet korrigálni, ha az alkalmazás ezt lehetővé teszi. Kevés számú bemenő adat esetén ezt a megoldást ajánljuk. Ha viszont sok bemenő adatra van szükség (például egy mátrix elemeit kell megadni), akkor érdemes azokat egy szöveges állományba előre beírni. Az alkalmazás tesztelésekor ez mindenképpen kifizetődő, de a felhasználó számára is áttekinthetőbb, ha az adatokat előzetesen egy szövegszerkesztőben láthatja. Sajnos hibás adat esetén a pánikszerű leállás az egyetlen biztos módszer, hiszen futás közben már nem lehet a szöveges állományon módosítani. Az eredmény szöveges állományba történő írásának az a kézzel fogható előnye, hogy az a program befejeződése után is olvasható lesz az eredmény. Egy szöveges állomány szabadon szerkeszthető, ami különösen nagy felelősséget ró az őt használó programra. A bolond biztos működés biztosítása általában igen nehéz, ezért formai megkötéseket szoktak 167

168 megfogalmazni a szöveges állományra nézve, amelynek a betartása a felhasználó felelőssége: ha az állomány nem megfelelő formájú, akkor a programnak nem kell jól működnie. Sokszor fordul elő, hogy a szöveges állományban elhelyezett azonos típusú értékeket egy tömbbe kell bemásolni. A tevékenység megkezdése előtt létre kell hozni a tömböt, ehhez pedig jó tudni, hogy hány érték beolvasására kerül majd sor, azaz mekkora lesz a tömb mérete. Ha ez a méret már a fordítási időben ismert állandó (konstans), akkor könnyű dolgunk van: definiálunk egy ilyen méretű tömböt (erre szinte mindegyik programozási nyelven van lehetőség), majd (egy for ciklussal) feltöltjük az elemeit az állományból olvasott értékekkel, feltéve, hogy az állomány hátralevő részéből megfelelő számú értéket ki lehet olvasni. Ekkor már csak az a kérdés, hogy a szöveges állomány tényleg tartalmazza a megadott számú adatot, és kell-e hibajelzést adni, ha nem. Sokszor olyan programot várnak tőlünk, amelyik számára csak futási időben derül ki a létrehozandó tömb mérete, de még a tömbbe szánt elemek beolvasása előtt. Ilyenkor a futás közben kell létrehoznunk a megadott méretű tömböt, amelyet utána az előbb ismertetett módon (egy for ciklussal) tölthetünk fel. Ha a választott programozási nyelv nem teszi lehetővé a futás közben történő tömbméret megadását (például Pascal nyelv), akkor egy kellően nagyméretű tömböt kell definiálnunk, futás közben beolvassuk a tömb tényleges méretét (remélve, hogy ez nem nagyobb, mint a maximális méret), ezt a méretet külön eltároljuk és egy for ciklussal feltöltjük a tömböt. Ennek a megoldásnak az a hátránya, hogy esetenként túl pazarló, mert túl nagy tömböt hozunk létre felesleges elemekkel, vagy éppen fordítva, a rossz előkalkuláció miatt nem elegendő méretű tömböt foglalunk le. Bonyolultabb a helyzet, ha a szöveges állomány csak a tömbbe szánt értékeket sorolja fel, és semmilyen formában nem áll rendelkezésünkre előre ezek száma. Ilyenkor több lehetőség közül választhatunk. Az egyik lehetőség az, amit már az előbb ismertettünk. Lefoglalunk egy kellően nagy méretű tömböt, majd addig olvassuk az újabb és újabb értékeket az állományból (egy while ciklussal), amíg vagy egy speciális, az elemek végét jelző értékhez vagy az állomány végére nem érünk. (Az 168

169 állomány végét is egy speciális karakter jelzi, de a beolvasást végző nyelvi elemek gyakran elfedik ezt, és más módon adják tudtunkra azt, hogy elértünk az állomány végére.) Hatékonyabb az a megoldás, amelyik egy fokozatosan nyújtózkodó tömböt alkalmaz. Ennek mérete az állományból történő (while ciklusos) olvasás során lépésről-lépésre nő, így a beolvasás végén éppen a kívánt méretű tömbbel fogunk rendelkezni. Az azonban külön vizsgálandó, hogy a választott programozási nyelv rendelkezik-e ilyen lehetőséggel (mint például a C++ nyelv vector<> típusa vagy a C# List típusa), vagy ha nem, megéri-e megteremteni a lehetőségét egy ilyen tömbnek. Ha az állomány a tömb elemei előtt tartalmazza a tömb méretét is 1. A szóba jöhető tömbméreteknek ismert a felső korlátja, akkor egy ilyen maximális méretű tömböt definiálunk fordítási időben, ennek elejére egy for ciklussal olvassuk be az elemeket. 2. futási időben hozzuk létre a megadott méretű tömböt, majd ebbe olvassuk be egy for ciklussal az elemeket. Ha az állomány csak a tömb elemeit tartalmazza, a méretét nem 3. A szóba jöhető tömbök méretének ismert a felső korlátja, akkor egy ilyen maximális méretű tömböt definiálunk fordítási időben, egy while ciklussal olvassuk be ennek elejére az elemeket. 4. Futás közben nyújtózkodó méretű tömböt használunk, az elemek olvasása egy while ciklussal történik ábra. Tömb állományból való feltöltésének módjai Megemlítjük, de hatékonysági szempontok miatt egyáltalán nem javasoljuk, azt a lehetőséget, hogy kétszer egymás után nyissuk meg olvasásra a szöveges állományt. Először csak azért, hogy megszámoljuk hány 169

170 érték van benne (ehhez az előbb javasolt while ciklus kell), ezt követően létrehozzuk a szükséges méretű tömböt, és egy második menetben megfelelő számú értéket kiolvasva az állományból feltöltjük a tömböt (a már korábban említett for ciklussal). Az adott jelig vagy az állomány végéig tartó while ciklussal végrehajtott olvasást többnyire előreolvasási technikával valósítjuk meg. Ez a megállapítás a legtöbb nyelvre, köztük a C-szerű nyelvekre (C++-ra is) is igaz. Ennek a feldolgozásnak az a lényege, hogy először megkíséreljük a soron következő érték beolvasását, majd csak ezt követően vizsgáljuk meg, hogy sikerült-e az olvasás (nem értünk-e az állomány végére, nem olvastunk-e speciális jelet, amely az adatok végét jelzi), és csak ezután dolgozzuk fel a beolvasott értéket. Egy ilyen feldolgozásban az olvasó utasítás a ciklusfeltétel ellenőrzése előtt kell, hogy megjelenjen. Az alábbi algoritmus-séma mutatja be az előreolvasási technikát. következő érték olvasása while( nincs fájlvége vagy a beolvasott érték nem speciális jel){ beolvasott érték feldolgozása következő érték olvasása Ebben az olvasó utasítás két helyen is szerepel: a ciklus előtt és a ciklus mag végén, mert ez biztosítja, hogy a ciklusfeltétel kiértékelése közvetlenül az olvasás után történjen. A bemutatott technikának az előnye az, hogy a beolvasandó adatok végét ugyanúgy kell vizsgálni akkor is, ha azt az állomány vége jelzi, vagy ha egy speciális jel. (A C++ nyelv lehetőséget ad arra, hogy a ciklusfeltétel helyén szerepeljen az olvasó utasítás, azaz formailag csak egyszer.) Nyelvi elemek 170

171 A szöveges állományok kezelése hasonlít a konzolos input-outputhoz. Ez különösen így van azoknál a programozási nyelveknél, ahol az adatok be- és kivitele adatfolyam-kezeléssel történik. Egy alkalmazás számára végül is mindegy, hogy egy bemenő adatfolyam a billentyűzetről vagy egy szöveges állományból származó karakterláncot fogad-e, az adatok adatfolyamból történő kiolvasására ez nincs hatással. Ugyanez mondható el a kiírásról is. Minden szöveges állománynak van egy úgynevezett fizikai neve (útvonal+név+kiterjesztés). Ez az, amivel a háttértárolón a szöveges állományt azonosítani lehet. A szöveges állományokra egy programban egy belső névvel szoktak hivatkozni: ez az állomány logikai neve. Adatfolyamok használatakor a logikai név valójában nem az állománynak, hanem annak az állománnyal összekapcsolt adatfolyam objektumnak a neve, amelyen keresztül tudunk az állományból olvasni vagy az állományba írni. Ennek ellenére cseppet sem zavaró, ha az adatfolyam objektumra úgy tekintünk, mint magára a szöveges állományra, a fájlra. Ez mutatkozik meg az alább bemutatott fogalmak elnevezésében (fájlnyitás, fájlbezárás, stb.) is. Amikor az alkalmazásban hozzákötjük a fizikai névhez a logikai nevet, megnyitjuk a szöveges állományt. Szöveges állományokat többnyire vagy kizárólag olvasásra, vagy kizárólag írásra használunk. A fájlnyitáskor meg kell vizsgálni, hogy a megnevezett szöveges állomány tényleg létezik-e, ha nem, hibajelzést kell adni, és vagy bekérni újra az állomány fizikai nevét (hátha rosszul adtuk meg), vagy le kell állítani az alkalmazást. Ha már nincs szükség a szöveges állományra, akkor le kell zárni annak használatát. A fájlbezárás elhagyása az írásra megnyitott fájl esetén adatvesztéshez vezethet. Egy szöveges állományba történő írás során ugyanis nem ugyanabban az ütemben kerülnek a karakterek az állományba, mint ahogy az alkalmazás utasításai ezt előírják. A háttértárolóra vonatkozó műveletek futásai ideje ugyanis nagyságrenddel lassúbb, mint a memória műveleteké, ezért a futtatási környezetet biztosító operációs rendszer összevárja a kiírandó adatok egy csoportját és azokat egyszerre, egy blokkban továbbítja a háttértárolónak. Lezáráskor az utolsó, még ki nem írt blokk adatai is kiíródnak az állományba. Az ifstream ifile 171

172 ofstream ofile definiál egy úgynevezett bementi illetve egy kimeneti adatfolyam objektumot (ezeknek a neve itt ifile és ofile). A fájlhasználathoz szükséges nyelvi elemeket az fstream könyvtár definiálja, ezért a programunk elején szükség lesz az #include <fstream> sorra. Az adatfolyamokhoz tartozó fájlok neveit az ifile.open(fnev.c_str()) ofile.open(fnev.c_str()) utasításokkal adhatjuk meg. Ezek a bemeneti adatfolyamhoz tartozó fájt meg is nyitják, a kimeneti adatfolyamhoz tartozót pedig létre is hozzák. Az adatfolyamhoz tartozó fájl nevét archaikus (C stílusú) karakterlánc formában kell megadnunk. Ehhez elég annyit tudni, hogy egy string típusú változóban (legyen a neve: str) tárolt (korszerű, C++ stílusú) sztringnek az str.c_str() adja meg az archaikus alakját. Az adatfolyam definíciója és a hozzátartozó fájl megnyitása összevonható egy lépésbe. ifstream ifile(fnev.c_str()) ofstream ofile(fnev.c_str()) A fájlok lezárására automatikusan sor kerül akkor, amikor az adatfolyam élettartama lejár (például a vezérlés az adatfolyam deklarációját tartalmazó blokk végéhez ér). A lezárás azonban explicit módon is kikényszeríthető: ifile.close() ofile.close() 172

173 Egy fájl megnyitásakor különféle hibák történhetnek. A leggyakoribb az, hogy a megnevezett állományt nem találjuk meg, mert vagy elfelejtettük létrehozni, vagy nem abban a könyvtárban van, ahol keressük. Az esetleges hibára a fail() függvénnyel kérdezhetünk rá. ifstream ifile; if(ifile.fail()){ cout << "Hiányzik az állomány\n"; return 1; Ciklusba is szervezhetjük a fájlnyitást, csak ilyenkor a fájlt azonosító adatfolyamot a clear() művelettel újra inicializálni kell, mielőtt újra megpróbáljuk megnyitni. Erre példát a feladatok megoldó kódjaiban mutatunk majd. Az olvasás az ifile >> változó, az írás az ofile << kifejezés utasítással történik, amely igencsak hasonlít a konzolos input-outputhoz. Akárcsak ott, most is szöveges adatfolyam objektumokkal van dolgunk, amelyekkel a szöveges állomány tartalmát, egy karakterláncot tudunk megfelelően szeletelve beolvasni, vagy a kiírásnál összeállítani. Fájlvége kezelése a programozási nyelvekben Amikor egy szöveges állományból olvasunk egy C++ nyelv nyújtotta adatfolyam segítségével, és az állomány karaktereit már sorban kiolvastuk, akkor tehát egy sikertelen olvasás után beállítódik egy speciális jelzőbit (eof flag), amely ezután azt mutatja, hogy az olvasás során az állomány végéhez értünk. Fontos tudni, hogy az állomány megnyitása nem inicializálja az eof flag értékét, azt csak az első olvasás teszi meg: sikeres olvasás esetén 0, sikertelen olvasás esetén 1. Az eof flag értékét közvetett módon az olvasáshoz használt adatfolyam eof() függvényével kérdezhetjük le: igaz érték ad vissza, ha a flag értéke 1, különben hamisat. 173

174 Ez a viselkedés az oka annak, hogy a C++ nyelven a már említett előre olvasási technikát kell alkalmazni. Előbb kell olvasni, és utána kiértékelni az olvasás eredményét. Az ismertebb programozási nyelvek ezt a megoldást követik. Ettől lényegesen eltér a Pascal programozási nyelv fájlkezelése. Ez a nyelv egy olyan fájlolvasási műveletet ajánl fel, amely sikertelen olvasási kísérlet esetén abortál. Ezért minden olvasás előtt meg kell vizsgálni, hogy elértük-e a fájl végét. Ehhez a fájlkezelés egy speciális mutatót használ, amely a szöveges állomány soron következő, még ki nem olvasott karakterére mutat. Ha ez a karakter a sorvége jel, akkor elértük a fájl végét. Ezt az eseményt itt is egy eof() függvénnyel kérdezhetjük le. De míg más nyelvekben az eof() függvény akkor ad vissza igaz értéket, amikor már kiolvastuk a fájlvége jelet, addig itt akkor, amikor elértük azt, de még nem olvastuk ki. A fájlból történő olvasás esetén a szöveges állomány aktuális, a már feldolgozott karakterek utáni első karakterétől kezdve próbál beolvasni egy a változó típusának megfelelő értéket. Alapértelmezett beállítás mellett átlépi az elválasztó jeleket (szóköz, tabulátor jel, sorvége jel), majd sorban olvassa azokat a karaktereket, amelyekből a változó számára érték állítható elő. Például, ha a változó egész típusú, akkor az első ilyen karakter lehet számjegy vagy egy előjel (+/-), az azt követő karakterek pedig számjegyek. Az olvasás addig tart, amíg nem következik egy nem-számjegy karakter. Legjobb, ha az értéket hordozó karaktersorozatot az állományban egy elválasztójellel zárjuk. A következő olvasás majd ennél a karakternél folytatódik. Hibás működést okoz, ha az olvasás nem tud megfelelő értéket előállítani, például ha egy a betűt talál egy egész típusú változó beolvasásához. Egy utasítással egyszerre, pontosabban egymás után több változónak is lehet értéket adni: ifile>>változó1>> változó2. C++ nyelven az olvasás nem okoz hibás működést, ha már elértük a fájl végét. Ilyenkor egyetlen dolog történik: ettől kezdve az ifile.eof() függvény igaz értéket ad vissza. Ügyeljünk arra, hogy mindaddig, amíg nem hajtottunk végre olvasást, addig az ifile.eof() függvény értéke nem megbízható. Tehát mindig előbb olvassunk, és csak utána vizsgáljuk meg az 174

175 eof()-ot. Ezt biztosítja az implementációs stratégiák között említett előre olvasási technika. Megjegyezzük, hogy a fail() függvény általánosabb hatású, mint az eof(), mert nemcsak fájl vége esetén, hanem egyéb olvasási hibák esetén is igazat ad. Bolond biztosabb lesz az alkalmazásunk, ha a fájlvége figyelést a fail() függvénnyel végezzük. Ha a szöveges állomány összes karakterét egyenként kell beolvasni, akkor vagy ki kell kapcsolni az elválasztójelek átlépését #include <iomanip> ifile.unsetf(ios::skipws); char ch; ifile >> ch; vagy másik műveletet kell választani: char ch; ifile.get(ch); Ez utóbbinak megvan a kiíró párja is: ofile.put(ch), amely egyetlen karakter kiírásánál ugyanúgy működik, mint az ofile << ch utasítás. Ha a szöveges állomány összes karakterét egyenként kell beolvasni, akkor vagy ki kell kapcsolni az elválasztójelek átlépését #include <iomanip> ifile.unsetf(ios::skipws); char ch; ifile >> ch; vagy másik műveletet kell választani: 175

176 char ch; ifile.get(ch); Ez utóbbinak megvan a kiíró párja is: ofile.put(ch), amely egyetlen karakter kiírásánál ugyanúgy működik, mint az ofile << ch utasítás. Sokszor van szükség arra, hogy teljes sorokat olvassunk be egyben, majd a beolvasott sorból szedjük ki a számunkra hasznos információt. A getline() függvénnyel tudunk egy teljes sort, string sor; getline(ifile, sor); vagy egy megadott jelig (karakterig) tartó karakterláncot beolvasni egy szöveges állományból. Ez az elválasztó jel lehet a sorvége jel is, ekkor az utasítás hatása megegyezik a fentivel. getline(ifile, sor, '\n' ); Nem tartozik szorosan a szöveges állományok témakörébe, de mivel az adatfolyamokkal kapcsolatos nyelvi lehetőség, ezért itt említjük meg az úgynevezett sztring-folyamok (stringstream) használatát. Ez lehetőséget nyújt arra, hogy egy tetszőleges sztringet, ugyanúgy, mint a konzolos input vagy a szöveges állomány beolvasása esetén, felszeleteljünk és a szeleteket megfelelő más típusú értékekké alakítsuk, vagy fordítva, ahogy a kiírásnál, különféle értékeket egyetlen karakterláncba fűzzünk össze. A sztringfolyamok használatához szükség van az #include <sstream> sorra. Helyezzünk el például egy szóközökkel tagolt szöveget egy input sztring-folyamba és olvassuk ki egyenként a szöveg szavait. Ehhez létrehozunk egy input sztring-folyamot (istringstream), beletesszük a szöveget tartalmazó sztringet (str()), és a >> operátorral kiolvassuk a sztring-folyamból a szavakat. Az alábbi kódban ezt az olvasó operátort a ciklusfeltétel helyére tettük: így egyrészt megvalósul az előreolvasás, hiszen a 176

177 ciklusmag mindig egy olvasással kezdődik, másrészt az olvasás egy logikai értéket ad vissza, amely hamis, ha az olvasás sikertelen. string str = "Alma a fa alatt"; istringstream is; is.str(str); string tmp; while(is >> tmp) { cout << tmp << endl; Természetesen eltérő típusú értékek is kiolvashatóak egy sztringből, csak tudni kell, hogy azokat milyen sorrendben fűztük korábban össze. string str = "218 Gipsz Jakab "; istringstream is; is.str(str); string family, first, birth; int id; double result; is >> id >> family >> first >> birth >> result; Az output sztring-folyam segítségével tetszőleges sztring állítható össze úgy, hogy közben a sztringbe fűzött adatelemek konverziójára is sor kerül: ostringstream os; os << "A " << 3.2 << " egy valós szám "; string str = os.str(); 177

178 Ebben a fejezetben találkozni fogunk a struktúra nyelvi elemmel. Ennek segítségével olyan összetett adattípust definiálhatunk, amelynek egy értéke több komponensből áll. Egy ilyen összetett érték, más néven rekord, komponenseire név szerint lehet hivatkozni. Az alábbi példa egy hallgató adatait (azonosító és két osztályzat) összefogó rekord típusát írja le: struct Hallg { ; string eha; int jegy1, jegy2; Ha h egy Hallg típusú változó, akkor a h-ban tárolt érték egy rekord, amelynek például az azonosítójára a h.eha kifejezéssel hivatkozunk, ezt a sztringet le lehet kérdezni, meg lehet változtatni. 178

179 11. Feladat: Szöveges állomány maximális eleme Olvassunk be egy szöveges állományból egész számokat egy tömbbe, és válasszuk ki a tömb elemei közül a legnagyobbat, továbbá mondjuk meg, hogy ez hányadik a számok között! Az állomány első eleme a feltöltendő tömb hosszát mutatja, amit aztán ennek megfelelő számú egész szám követ. Feltesszük, hogy az állományban csak egész számokat helyeztek el elválasztó jelekkel (szóköz, tabulátor, sorvége) határolva. Az eredményt írjuk ki a szabványos kimenetre (képernyőre)! A feladat csak a bemenő adatok megadási módjában tér el a 6. feladatban megoldott problémától, így a megoldás terve megegyezik az ott látottakkal. Specifikáció A = ( v : Z n, max, ind : Z ) Ef = ( v=v n>0 ) Uf = ( v=v max = v[ind] = max n i 1 v[i] ind [1..n] Absztrakt program A feladat visszavezethető a maximum kiválasztás tömbökre adaptált programozási tételére: max, ind := v[1], 1 i = 2.. n v[i]>max max, ind := v[i], i SKIP Implementálás 179

180 Csak a tömb szöveges állományból történő feltöltése résszel kell foglalkoznunk, a kód többi része megegyezik a 6. feladatnál mutatott kódrészekkel. Először megkérdezzük annak a szöveges állománynak a nevét, amelyben a bemenő adatokat tároljuk. Ezt a szabványos bementről olvassuk be egy sztringbe. string filename; cout << "A fájl neve:"; cin >> filename; Ezután definiálunk és megnyitunk egy bementi adatfolyamot, amely segítségével a szöveges állomány adatait elérhetjük. Ennek az objektumnak a neve legyen inp, amelyet ifstream típusúnak választunk. Ennek a típusnak a használatához hivatkoznunk kell a program elején az fstream könyvtárra (#include <fstream>). Az inp megnyitásakor (open) kell megadnunk a szöveges állomány már bekért nevét archaikus (C-stílusú) karakterlánc formában: filename.c_str(). Mivel a fájl megnyitásakor különféle hibák fordulhatnak elő (nincs állomány, más néven szerepel, nincs jogunk létrehozni, stb.), felkészülünk azok észlelésére, és a felhasználó tájékoztatása után megkíséreljük újra a műveletet. Az alábbi kód az input szöveges állományt nyitja meg, ennek sikerességét az inp.fail() logikai kifejezés vizsgálatával ellenőrizzük. A clear művelet az ismételt fájlnyitáshoz kell. ifstream inp; do{ string filename; cout << "\nkérem a fájl nevét: " ; cin >> filename; inp.clear(); 180

181 inp.open(filenev.c_str()); if(inp.fail()) cout << "A megadott fájlt nem találom! \n"; while(inp.fail()); A sikeres megnyitás után beolvashatjuk az állományban tárolt adatokat egy tömbbe. A tömb definíciója előtt meg kell ismerni a tömb elemeinek számát, azaz a tömb hosszát. Ebben a feladatban ezt a szöveges állomány tartalmazza, így először onnan a feldolgozásra váró elemek számát olvassuk be. Az olvasás módja azonos a szabványos bementről történő olvasással, azzal a különbséggel, hogy itt nem a cin, hanem az inp bemeneti adatfolyamot használjuk. int n; inp >> n; Meg kell győződnünk arról, hogy ez az olvasás sikerült-e, azaz inp.fail() hamis-e, valamint azt, hogy teljesül-e a darabszámra a feladat előfeltétele: ez előírja, hogy a tömb hossza egy pozitív egész szám. int n; inp >> n; if (inp.fail() n<1){ cout << "Az állomány tartalma nem megfelelő!\n"; return 1; Megjegyezzük, hogy itt szó sem lehet arról, hogy valamilyen do-while ciklussal végezzük az adatellenőrzést. A szöveges állomány ugyanis már 181

182 készen van, olvasásra megnyitottuk, ezért futás közben nincs lehetőség a tartalmának megváltoztatására. Hibás adat esetén le kell állítani a programot, hogy a szöveges állományt kijavíthassuk. Megfelelő darabszám ismeretében létrehozhatjuk azt a tömböt, amelyben a bemenő értékeket helyezzük el. Ha feltételezhetjük, hogy az állomány elején megadott darabszám helyes, azaz a darabszámot annyi szám követi az állományban, amennyi a darabszám értéke, akkor az alábbi kóddal feltölthetjük a tömböt. vector<int> v(n); for(int i=0; i<n; ++i) inp >> v[i]; Ha a darabszám kisebb a szöveges állományban ténylegesen elhelyezett számok számánál, akkor nem fog minden szám bekerülni a tömbbe; ha nagyobb, akkor a tömb végén lesznek definiálatlan értékek is. Az utóbbi esetben a maximum kiválasztás hibás eredményt is okozhat. Éppen ezért biztonságosabb az alábbi kód, amelyik leállítja az olvasást, ha az végére ért az állománynak, és figyeli azt is, hogy az egyes számok beolvasása rendben történt-e. vector<int> v(n); for(int i=0; i<n; ++i){ inp >> v[i]; if(inp.eof()) break; if(inp.fail()){ cout << "Az állomány tartalma nem jó!\n"; return 1; 182

183 A beolvasás után a tömb elemeit kiírjuk a szabványos kimenetre. cout << "A tömb hossza: " << n << endl; cout << "A tömb elemei: "; for (int i=0; i<n-1; ++i) cout << v[i] << ", "; cout << v[n-1] << endl; Tesztelés A teszteléshez a 6. feladat teszteseteit használhatjuk. Ezeken kívül a beolvasás számára kell fehér doboz teszteseteket megfogalmazni. 1. Nem létező állomány megadása illetve ismételten nem létező állomány megadása. 2. Üres állomány. 3. Első adat nem természetes szám. 4. Első szám nem pozitív. 5. Az első szám nem az azt követő elemek darabszáma (nagyobb ill. kisebb). 6. Az állományban számok helyett szövegek vannak. 7. Az állományban túl nagy szám is van. 183

184 Teljes program Tekintsük meg végül a teljes programkódot! #include <iostream> #include <fstream> #include <vector> #include <string> using namespace std; int main() { // Az állomány nevének bekérése string filename; ifstream inp; do{ cout << "\nkérem a fájl nevét: " ; cin >> filename; inp.clear(); inp.open(filename.c_str()); if(inp.fail()) cout << "A megadott fájlt nem találom!\n"; while(inp.fail()); // Beolvassuk és ellenőrizzük a tömb hosszát 184

185 int n; inp >> n; if (inp.fail() n<1){ cout << "Az állomány tartalma nem jó!\n"; return 1; // Létrehozunk egy tömböt, feltöltjük, majd kiírjuk vector<int> v(n); for(int i=0; i<n; ++i){ inp >> v[i]; if(inp.eof()) break; if(inp.fail()){ cout << "Az állomány tartalma nem jó!\n"; return 1; cout << "A tömb hossza: " << n << endl; cout << "A tömb elemei: "; for (int i=0; i<n-1; ++i) cout << v[i] << ", "; cout << v[n-1] << endl; 185

186 // Maximum kiválasztás int ind = 0, max = v[0]; for(int i=1;i<n;++i){ if (v[i]>max){ ind = i; max = v[i]; // Kiíratás cout << "A tömb egyik legnagyobb eleme: " << max << endl; cout << "Ez a " << (ind+1) << ". elem." << endl; return 0; 186

187 12. Feladat: Jó tanulók kiválogatása Válogassuk ki egy tömbben tárolt hallgatói adatok közül a jó tanulókat! Jó tanuló az, akinek nincs négyesnél rosszabb jegye. Ehhez ismerjük a hallgatók kódját és két osztályzatát. Gyűjtsük ki azon hallgatók kódjait, akiknek mindkét osztályzata legalább négyes! A tömböt egy szöveges állományból töltsük fel. A szöveges állományban soronként helyezkednek el a hallgatók adatai. Egy sorban az első hét karakter a kód, utána egy szóköz, azután az első osztályzat (számjegy), majd egy szóköz, és a második osztályzat. Specifikáció A = ( adat : Hallg n, ki : String* ) Hallg=rec(eha:String, jegy1,jegy2:n) Ef = ( v=v ) n Uf = ( v=v ki = adat[ i]. eha i 1 adat[ i]. jegy adat[ i]. jegy Absztrakt program A feladat visszavezethető egy olyan összegzésre, ahol a hozzáadás művelete helyett a hozzáfűzés műveletét használjuk, de csak az utófeltételben leírt feltétel teljesülése esetén: ) ki := <> i = 1.. n adat[i].jegy1>3 adat[i].jegy2>3 ki := ki <adat[i].eha> SKIP Implementálás 187

188 A programkód két fő részből áll: először feltöltjük az adat tömböt a szöveges állománybeli adatokkal, utána pedig elvégezzük a kiválogatást. Külön kiírás részre nincs szükség, ha az implementációban a ki kimeneti-változót a cout kimeneti adatfolyammal helyettesítjük. Erre az ad lehetőséget, hogy a kimeneti-változó egy kód sorozatot tárol és egy kód egy sztring, tehát összességében az eredmény egy karakterfolyam, amelyet közvetlenül a cout kimeneti adatcsatornára küldhetünk. Először azonban definiáljuk a hallgatói adatokat tartalmazó struktúrát. struct Hallg { string eha; int jegy1, jegy2; ; A hallgató a feladat szempontjából egy összetett, több részből álló adat: a hallgató kódja és két osztályzata. Ezt a fenti kóddal írhatjuk le. A Hallg egy új típus, amelynek értékei a fenti három adattal rendelkező hallgatók lehetnek. Segítségével definiálhatunk változókat Hallg h amelyek képesek egy kód és két osztályzat tárolására. Az egyes részekre h.eha, h.jegy1, h.jegy2 formában lehet hivatkozni. A Hallg típust használhatjuk egy olyan tömb deklarálására is, amelyik elemei hallgatók: Beolvasás vector<hallg> adat Olvassuk be először a szöveges állomány nevét a billentyűzetről az előző feladatban látott ellenőrzéssel, és nyissuk meg az állományt olvasásra. Ezután beolvashatjuk az állományban tárolt adatokat egy tömbbe. A tömb méretét csak azután fogjuk megismerni, miután az összes adatot beolvastuk, ugyanis most ez ellentétben az előző feladattal nincs explicit módon 188

189 tárolva a szöveges állomány elején. Szerencsére egy vector<> típusú tömbhöz hozzá is lehet fűzni elemeket. vector<hallg> adat; Hallg h; adat.push_back(h); Egy hallgató adatainak a szöveges állományból való beolvasására két megoldás is kínálkozik. Az egyikben azt használhatjuk ki, hogy a szöveges állományban a szabványos elválasztó jelekkel (szóköz, sorvége jel) vannak az adatelemek egymástól elhatárolva. inp >> h.eha >> h.jegy1 >> h.jegy2; A másik módszernél azt használjuk ki, hogy az összetartozó adatok soronként helyezkednek el, és minden soron belül rögzített egy adatelem kezdőpozíciója és mérete. A kód a 0. pozíción kezdődik és 7 karakter hosszú, az első jegy a 8., a második jegy a 10. pozíción található. Először beolvassuk a teljes sort egy sztringbe, string sor; getline(inp, sor); majd ebből mazsolázzuk ki az adatelemeket. A substr() egy karakterlánc megadott pozíciójú karakterétől kezdődően megadott hosszú rész-sztringjét adja vissza. h.eha = sor.substr(0,7); Ha egy adatelem nem sztring, akkor azt át is kell alakítani megfelelő típusúra (ezt az előző módszernél a beolvasó operátor elvégezte helyettünk) h.jegy1 = atoi(sor.substr(8,1).c_str()); h.jegy2 = atoi(sor.substr(10,1).c_str()); 189

190 Az atoi() helyett használható az alábbi megoldás is: istringstream is; // #include <sstream> is.str(sor.substr(8,1)); is >> h.jegy1; is.clear(); is.str(sor.substr(10,1)); is >> h.jegy2; A fenti módszerek mindegyike feltételezi, hogy a szöveges állomány tényleg a megadott formában tárolja az adatokat. Egyértelmű, hogy ennél a feladatnál a legelső olvasási módszer a jobb, mert egyszerűbb. Nem működne viszont abban az esetben, ha a kódok helyett nevek lennének a szöveges állományban, hiszen név tartalmaz szóközt is, amely a beolvasás szempontjából elválasztó jel. (Ráadásul azt sem tudjuk, hogy hány szóköz van egy névben, hiszen egy hallgatónak több keresztneve is lehet.) Az adatoknak a szöveges állományból való beolvasását végző ciklust előre-olvasási technikával kell kódolni, azaz a ciklus előtt és a ciklus mag végén is olvasni kell a fájlból. A ciklusfeltételben vizsgálhatjuk, hogy nem értünk-e a fájl végére. (Erre az alábbiakban az eof() függvény helyett a fail()-t használjuk.) Az előre-olvasást végző ciklus magjában mindig az egy lépéssel korábban beolvasott adatokat dolgozzuk fel, azokat fűzzük hozzá a tömbhöz. vector<hallg> adat; Hallg h; inp >> h.eha >> h.jegy1 >> h.jegy2; while(!inp.fail()) { 190

191 adat.push_back(h); inp >> h.eha >> h.jegy1 >> h.jegy2; Absztrakt program Az absztrakt program kódolásánál figyelembe kell venni, hogy nincs szükség a ki-t helyettesítő cout kezdeti értékadására. A hozzáfűzés műveletét a kiíró operátor (<<) helyettesíti. Ne felejtsük el a kiírt kódokat valahogyan elválasztani egymástól, például mindegyik kerüljön új sorba. Ügyeljünk arra is, hogy a C++ nyelvbeli tömb 0-tól indexeli az elemeit, ezért a tervben szereplő 1..n intervallumot a 0..n-1 intervallum váltja fel. cout << "A jó tanulók kódjai:\n"; for(int i=0; i<(int)adat.size(); ++i){ if(adat[i].jegy1>3 && adat[i].jegy2>3){ cout << adat[i].eha << endl; Tesztelés A teszteléshez a 6. feladata teszteseteit használhatjuk. Ezeken kívül a beolvasás számára kell fehér doboz teszteseteket megfogalmazni. 1. Nem létező állomány megadása illetve ismételten nem létező állomány megadása. 2. Üres állomány. 3. Első adat nem természetes szám. 191

192 4. Első szám nem pozitív. 5. Az első szám nem az azt követő elemek darabszáma (nagyobb ill. kisebb). 6. Az elemek között van nem egész szám. 192

193 Teljes program #include <iostream> #include <fstream> #include <sstream> #include <vector> #include <string> using namespace std; struct Hallg{ string eha; int jegy1, jegy2; ; int main() { ifstream inp; do{ string fajlnev; cout << "\nkérem a fájl nevét: " ; cin >> fajlnev; inp.clear(); inp.open(fajlnev.c_str()); if(inp.fail()){ 193

194 cout << "A megadott fájlt nem találom! \n"; while(inp.fail()); vector<hallg> adat; Hallg h; inp >> h.eha >> h.jegy1 >> h.jegy2; while(!inp.fail()) { adat.push_back(h); inp >> h.eha >> h.jegy1 >> h.jegy2; cout << "A jó tanulók kódjai:\n"; for(int i=0; i<(int)adat.size(); ++i){ if(adat[i].jegy1>3 && adat[i].jegy2>3){ cout << adat[i].eha << endl; return 0; C++ kislexikon 194

195 Szöveges állományok kezelését támogató csomag Szöveges állományhoz adatcsatorna definiálása Szöveges állomány megnyitása olvasásra (output fájl ugyanígy) #include <fstream> ifstream f; ofstream f; ifstream f("fajlnev.txt"); string filename = "fajlnev.txt"; ifstream f(filename.c_str()); ifstream f; f.open("fajlnev.txt"); Szöveges állomány megnyitása ellenőrzéssel ifstream f(filename.c_str()); if(f.fail()){ cout << "Nyitási hiba!\n "; return 1; ifstream f; do{ string fnev; cout << "\nfájl neve: "; cin >> fnev; f.clear(); f.open(fnev.c_str()); if(f.fail()) cout << "Nyitási hiba!\n"; 195

196 while(!f.fail()) f >> valtozo; Olvasás szöveges állományból char ch; f.get(ch); //#include <iomanip> f.unsetf(ios::skipws) string sor; char ch; f >> ch; getline(f,sor); Írás szöveges állományba f << kifejezes; f << kifejezes1 << kifejezés2; Szöveges állomány lezárása f.close(); Tömb feltöltése szöveges állományból előre megadott mérettel Element t[size]; for(int i=0; i<size; ++i){ f >> t[i]; 196

197 Fokozatosan nyújtózkodó tömb feltöltése szöveges állományból előre olvasási technikával fájl végéig vector<element> t; Element item; f >> item; while(!f.fail()) { t.push_back(item); f >> item; Struktúra struct Sample { string field1; int field2, field3; double field4; ; Sample v; v.field1 = " "; int a = v.field3; Konverzió sztringre #include <sstream> ostringstream os; os << 23.5 << "---" << 12 string str = os.str(); Elválasztó jelekkel tűzdelt sztring elemeinek típusos olvasása string str = "Alma 2 fa -12.4"; istringstream is; is.str(str); 197

198 int n; string s; double f; is >> word >> n >> word >> f; 198

199 II. RÉSZ PROCEDURÁLIS PROGRAMOZÁS Egy összetett feladat megoldását jól körülhatárolható részfeladatok megoldásainak összességeként készíthetjük el. Könyvünk első kötetében részletesen is bemutattuk, hogyan lehet a visszavezetés programtervezési módszerének segítségével logikailag önálló részekből felépíteni egy programot (lásd I. kötet 6. fejezet). Ott bevezettük az alprogram fogalmát, amelyek alkalmazásával már a tervezési szakaszban megjelent a procedurális programozás. Az alprogramok önállóan tervezhetők és kódolhatók. Működésük egyik jellemzője, hogy bizonyos pontokon átadják a vezérlést egy másik programrésznek (alprogramnak), majd annak befejeződése után folytatják a tevékenységüket, azaz a program összességében különféle alprogramok (procedúrák) működésének láncolata. A megvalósítás szintjén feltéve, hogy a választott programozási nyelv rendelkezik megfelelő nyelvi eszközökkel az alprogramokon kívül más jellegű programrészeket is lehet különíteni egymástól egy programban. Ilyen lehet például egy kizárólag adatokat tároló programrész (például tömbök gyűjteménye) vagy valamilyen szempontból egy csoportot alkotó alprogramok listája. Ezeket a leírt kódban is jól olvashatóan elkülönülő programrészeket moduloknak nevezzük, alkalmazásuk pedig moduláris programozás, amely a procedurális programozás kiterjesztésének is tekinthető. Egy moduláris program működési logikája kétszintűvé válik: külön szintet képeznek az önmagukban vizsgálható részei, és külön vizsgálhatók azok egymáshoz való viszonya. Ez a szemléletmód hathatósan segíti mind a program tervezését, mind a megvalósítását, mind tesztelését. Az programrészek kialakításánál egyszerre kell ügyelni a helyes szétválasztásra és az elválasztott részek összekapcsolásra. Egyfelől el kell tudni úgy különíteni ezeket egymástól, hogy egy-egy résznek bármilyen változtatása csak lokális következményekkel járjon a program egészére nézve és ennek az elvnek következményeként minél közelebb kerüljön egymáshoz a programrész működési logikája és az általa használt adatok. Másfelől meg kell oldani az elkülönített programrészek közötti adat- és 199

200 üzenetkommunikációt. Fontos, hogy az egyes részek közötti kapcsolat egyszerű, jól átlátható legyen, minél inkább szolgálja az egyes részek önállóságát, és szoros legyen az elkülönített részeken belüli összetartozás. Már a korai programozási nyelvek is rendelkeztek olyan nyelvi elemekkel, amelyekkel egy-egy részprogramot ki lehetett jelölni egy programon belül. A mai nyelvekben már számos eszköz található a program egyes részeinek leírására. Végső soron már ilyen az is, ha kommentekkel elhatárolva különítünk el egy-egy kódrészletet vagy úgynevezett utasítás blokkba zárjuk azt, de a procedurális programozás elsőszámú nyelvi elemei kétségkívül az alprogramokat kódszinten kifejezni képes programfüggvények és eljárások. Az által, hogy a programunk hierarchiája a részekre bontás során bonyolultabbá válik, még inkább előtérbe kerül annak az igénye, hogy egyegy rész kódjai könnyen megérthetőek legyen. Ennek egyik biztosítéka az, ha programunk kódját viszonylag egyszerű, szabványos kódrészletekből építjük fel. Szabványos kódrészen azt értjük, amikor hasonló részfeladatokra mindig ugyanazon minta alapján készítjük a kódot. Több száz egymástól lényegesen különböző alkalmazást készíthetünk, de ha mindegyikben szükség van például egy tömb szöveges állományból történő feltöltésére, akkor semmi okunk arra, hogy ezt a részt ne ugyanúgy írjuk meg mindig. A program attól lesz biztonságos (jól olvasható, áttekinthető, ezért könnyen javítható, módosítható), ha a hasonló részfeladatokat mindig azonos módon, jól átgondolt elemekkel kódoljuk. A programozói virtuozitást ugyanis nem abban kell kiélni, hogy hányféle kóddal tudunk például egy tömböt feltölteni, hanem abban, hogy szabványos elemekből építkezve hogyan lehet újabb és újabb, minél változatosabb problémákat megoldani. Természetesen némi gyakorlást igényel annak eldöntése, hogy milyen kódrészeket érdemes mintaként megjegyezni, de talán segít ebben az a kollektív programozói tapasztalat is, amit például ez a könyv is sugall. Ebben a részben kódmintának egyrészt a programozási tételek kódjait, másrészt a különféle beolvasást illetve kiírást végző kódrészeket tekintjük, amelyeken csak kisebb változtatásokat szabad végezni, például átnevezhetjük benne a változókat, de a vezérlési szerkezeteik, formájuk nem változhat. 200

201 A programjaink modulokra bontása az alkalmazásaink tesztelésére is kihat. A tesztelést is két szinten végezhetjük: külön-külön tesztelhetőek az egyes programrészek (ezt általában modultesztnek hívjuk, ami ebben a részben az egyes alprogramok tesztelését jelenti), majd külön azok kapcsolatainak tesztelése. Fontos, hogy egy alprogram tesztelésénél olyan esetekre is gondoljunk, amely az alkalmazás futtatása során ugyan soha nem állhatna elő, mert az alprogram hívására olyan környezetben kerül sor, amely eleve kizárja bizonyos paraméterek kipróbálását. Ha azonban az alprogramot a környezetéből kiragadva egy másik alkalmazásban is fel akarjuk használni, nem lenne jó, ha a futása produkálhat teszteletlen eseteket is. Ezért sok esetben egy alprogram teszteléséhez külön tesztkörnyezetet, az alprogramot meghívó speciális főprogramot kell készíteni. 201

202 6. Alprogramok a kódban Programjainkban a funkcionálisan összetartozó utasításokat, azaz egy-egy részfeladat megoldásáért felelős programrészeket külön egységekbe, úgynevezett alprogramokba (procedúrákba) szervezhetjük. Egy alprogram olyan önálló kódrész, amelyiknek a végrehajtását egy speciális utasítás, az alprogram hívása kezdeményez. Ekkor átkerül a vezérlés az alprogramhoz, majd annak befejeződésekor a hívó utasítás utáni pontra tér vissza. Egy alprogram nem független az őt tartalmazó programtól, adatokat kap tőle és adatokat ad neki vissza. Az adatáramlás többféle módon is megvalósulhat az alprogram és környezete között. Ha az alprogramba bekerülő és belőle kikerülő adatokat egyértelműen jellemezzük, akkor paradox módon egy alprogramokra tagolt program sokkal erősebb kohéziót valósít meg a program egyes részei között, mint egy monolit, alprogramokra nem tördelt program, mert felügyeltté válik az egyes részek közötti adatcsere. Implementációs stratégia Egy programkódban többféle szempont alapján lehet alprogramokat kialakítani. 1. Önálló funkciót betöltő, sokszor már a tervezésnél felfedett jól körülhatárolt résztevékenységet végző programrész elkülönített kódolása. 2. Programban több helyen megismétlődő kódrész egy helyen való közös leírása, amellyel csökkenthető a kód mérete és biztonságosabb lesz a javítása ábra. Alprogram kialakításának okai 202

203 Sok alprogram már a tervezés során körvonalazódik, hiszen ekkor derül ki, hogy a programnak milyen funkciókat ellátó részei vannak. Nem törvényszerű, de ajánlott, hogy ezek a funkcionálisan elkülöníthető programrészek az implementáció során is külön alprogramokat alkossanak. Az implementáció során olyan további funkciók megvalósítására is sor kerülhet (például adatbeolvasás, adatkiírás), amelyről a tervezés során még nem beszélünk. Ezeket is célszerű külön alprogramokba szervezni. Amikor a kódban több helyen is ismétlődő kódrészletet találunk, akkor érdemes azokat egy alprogramba összevonni, és ahol szükség van rá, onnan meghívni. Így rövidebb lesz a kód, de a sokkal fontosabb szempont az, hogy ha javítani kell egy ilyen ismétlődő kódrészben, akkor azt, annak alprogramba szervezése után, csak egy helyen kell megtenni. Az ismétlődő kódrészek alprogramba történő kiemelése akkor is követendő út, ha az így kiváltott kódrészek kismértékben eltérnek egymástól (például ugyanazt a tevékenységet más változókon végzik vagy tevékenységük kimenetele egy adat értékétől függ, stb.). Megfelelő általánosítással ugyanis akár egészen különböző kódrészeket is ki lehet váltani egyetlen, jól paraméterezhető alprogrammal. Természetesen ilyenkor a hívásnál a működést befolyásoló információkat át kell adunk az alprogramnak. Az már nehezen dönthető el, nem is lehet rá receptkönyvszerű választ adni, hogy meddig érdemes elmenni a kódrészek ilyen általánosításának irányába. Sokszor alkalmazott implementációs elv az, hogy egy alprogram kódját egyszerre lássuk fejlesztés közben a képernyőn. Ha ez nem állna fenn, akkor tagoljuk a kódot részekre, a részeket csomagoljuk külön alprogramokba. Ennek az elvnek az alkalmazása azt eredményezi, hogy egy alprogramban nem fog egy-két ciklusnál több szerepelni, és ha a ciklus magja túl nagy lenne, akkor azt is külön alprogramba, alprogramokba tagoljuk. Fontos kérdés, hogy egy alprogram hogyan tart kapcsolatot a program többi részével, hogyan valósul meg az adatáramlás a hívó program és a hívott alprogram között. Az egyik lehetőség erre a globális változók használata. Globális változó az, amelyet az alprogramon kívül definiálunk, de az alprogramban is látható. (Egy változó globális jelzője relatív fogalom, mindig egy alprogram vagy egyéb programblokk szempontjából értelmezhetjük.) Ha egy változó két 203

204 alprogramra nézve is globális, akkor azt mindkettő használhatja: olvashatja és felülírhatja. A globális változók használata első látásra egy igen egyszerű formája az adatcserének, de nagymértékben rontja a program áttekinthetőségét. Egy alprogram működésének megértését ugyanis akadályozza, ha minduntalan ki kell tekinteni az alprogramból, és megnézni, hogy egy alprogramban használt globális változó a program melyik részén keletkezett, mikor, milyen értéket kapott, hol lesz még felhasználva, stb. Ezért csak nagyon indokolt esetben engedélyezzük a globális változók használatát. Ilyen lehet például az, ha egy adatot az összes alprogram használja, és megjegyzésként pontosan megjelöljük, hogy melyik alprogram olvassa ezt az adatot, melyik az, amelyik meg is változtatja, továbbá erősen korlátozzuk az adat értékét megváltoztató alprogramok számát. Globális változó: Már az alprogramon hívása előtt létrehozott olyan változó, amelyet az alprogram is használhat. Lokális változó: Kizárólag az alprogramban használt változó. Bemenő paraméterváltozó: Az alprogram olyan lokális változója, amely az alprogram hívásakor a környezetétől (annak egy változójától vagy kifejezésétől) kezdő értéket kap. Eredmény paraméterváltozó: Az alprogram olyan lokális változója, 6-2. ábra. Alprogram változói implementációs szempontból Az alprogrammal való kapcsolattartásnak sokkal biztonságosabb megoldása az, ha az alprogram kizárólag a lokális változóit használja, és ezek közül jelöli ki azokat, amelyeken keresztül adatcserét hajthat végre a hívó programrésszel. Ezek az úgynevezett paraméterváltozók. Az alprogram formális paraméterlistája sorolja fel az alprogram paraméterváltozóit, rögzíti azok típusát és sorrendjét. Külön meg kell jelölni, hogy mely paraméterváltozók bonyolítanak bemenő adatforgalmat, és melyek kimenőt. A bemenő paraméterváltozók azok, amelyek a hívás során a hívóprogram 204

205 egy kifejezésének (akár egyetlen változójának) értékét kapják kezdőértékül. Eredmény paraméterváltozó az, amelyik az alprogram befejeződésekor visszaadja az értékét a hívás helyére, a hívó program egy arra kijelölt változójának. Egy paraméterváltozó lehet egyszerre bemenő- és eredmény paraméterváltozó is. Ebben az esetben a hívó program ugyanazon változójának adja vissza az értékét, amelytől a kezdőértéket kapta. Az alprogram hívásánál az úgynevezett aktuális paraméterlistát kell megadni, amely tartalmazza azokat a kifejezéseket, amelyek értékét a bemenő paraméterváltozóknak szánjuk, illetve azokat a változókat, amelyek az eredmény paraméterváltozóktól kapják majd az értéküket. A hívásnak egyértelműen ki kell jelölnie, hogy melyik paraméter melyik paraméterváltozóhoz tartozik. Ezt általában a paraméterek sorrendje határozza meg. Ügyelni kell arra, hogy egy paraméter típusa megegyezzen (legalább kompatibilis legyen) a neki megfeleltetett paraméterváltozó típusával. Egy alprogramot kétféleképpen hívhatunk meg. Függvényszerű hívása esetén az alprogramot függvénynek szokták nevezni, egyébként pedig eljárásnak. Eljárásként hívott alprogram esetén a hívás egy önálló utasítás, míg a függvény hívása egy utasításba ágyazott kifejezés. Mindkettő az alprogram nevéből, és az aktuális paraméterlistából áll. A függvényt hívó kifejezésnek maga a függvény ad értéket. Ehhez a függvény definiálásakor fel kell tüntetni a visszatérési érték típusát (esetleg típusait), és a kódjában egyértelműen jelölni kell azt, hogy leállásakor milyen érték adódjon vissza a hívás helyére. Elsősorban implementációs döntés (bár a tervezés is utalhat rá) az, hogy egy alprogramot függvényként vagy eljárásként kódoljunk-e. Szerencsére viszonylag könnyen lehet egy alprogramot átalakítani függvényből eljárásba és viszont. A döntésre hatással van az, hogy a választott programozási nyelv mit enged meg. Van ugyanis olyan nyelv, amely függvényei egyetlen egyszerű típusú visszatérési értékkel rendelkezhetnek csupán, van olyan, amelyikben a visszatérési érték mellett eredmény paraméterváltozó is használható, akad olyan is, amelyikben csak függvényeket használhatunk. Nyelvi elemek 205

206 A program alprogramokra tördelését a magas szintű programozási nyelvek különleges nyelvi elemekkel támogatják (szubrutin, eljárás, függvény). Egy alprogram egy programozási nyelvben egy deklarációból (fejből) és egy törzsből áll. A deklarációt és a törzset együtt az alprogram definíciójának hívjuk. A deklaráció tartalmazza az alprogram nevét, a formális paraméterlistát és az esetleges visszatérési érték típusát. Ha a deklarációból elvesszük a függvény nevét, akkor az így kapott maradékot a függvény típusának hívjuk. A törzs egy olyan programblokk, melynek végrehajtását a program bármelyik olyan helyéről lehet kezdeményezni, ahol az alprogram neve érvényes (ahová az alprogram hatásköre kiterjed). Az alprogram hívása az alprogram nevével történik. Ilyenkor a vezérlés (az utasítások végrehajtásának menete) átadódik a hívás helyéről az alprogram első utasítására. A hívó program további végrehajtása mindaddig szünetel, amíg az alprogramhoz tartozó kód le nem fut. Az alprogram akkor fejeződik be, ha az összes utasítása végrehajtódott vagy egy befejezését előíró speciális return utasításhoz nem ér. Ekkor a program végrehajtása visszakerül a hívás helyére. A hívó utasításban az alprogram neve mellett kell felsorolni az aktuális paramétereket, amelyek számra, sorrendre (bizonyos programozási nyelveknél ez nem kötelező) és típusra meg kell, hogy egyezzenek a formális paraméterlista változóinak típusával. (Vannak olyan programozási nyelvek, ahol a formális paramétereknek lehetnek alapértelmezett értékeik. Ilyenkor az aktuális paraméterlista rövidebb lehet a formális paraméterlistánál.) Egy bemenő adatként szereplő aktuális paraméter lehet a hívó programrész egy változója vagy egy kifejezés, ellenben a visszakapott adatként szereplő paraméter mindig a hívó programrész egy változója kell legyen. A paraméterváltozók az alprogram lokális változói, csak az alprogramon belül érvényesek (csak az alprogram törzsében lehet rájuk hivatkozni, az alprogram futási ideje alatt foglalnak helyet a verem memóriában). Ezek mellett az alprogramnak lehetnek egyéb lokális változói is. A paraméterváltozókat csak az különbözteti meg a többi lokális változótól, hogy az alprogram hívásakor illetve befejeződésekor adatkapcsolatot létesítenek a hívó környezettel. 206

207 A C++ nyelv kétféle paraméter átadási módot ismer. Érték szerinti paraméterátadással a bemenő adatot tudjuk a bemenő paraméterváltozóhoz eljuttatni, a hivatkozás (referencia) szerinti paraméterátadás mindkét irányú adatáramlást támogatja. Érték szerinti paraméterátadás valósul meg az alábbi kódban. Itt az x változó egy bemenő paraméterváltozó. Híváskor az v változó értéke átmásolódik az x változóba, amely egy önálló memória területtel rendelkező lokális változója a függvénynek. A hívás után már semmi kapcsolat nincs a v és az x változók között. hívás: int v = 23; fv(v); hívott: void fv(int x) { Paraméterátadási módok Érték szerinti paraméterátadás Az aktuális paraméter értéke az alprogram hívásakor átmásolódik a neki megfeleltetett paraméterváltozóba, és annak kezdőértékévé válik. A paraméterváltozó ettől eltekintve a hívó programrésztől teljesen függetlenül, az alprogram lokális változójaként használható. Cím vagy hivatkozás (referencia) szerinti paraméterátadás Az alprogram paraméterváltozója nem rendelkezik olyan önálló memóriafoglalással, ahová az aktuális paraméter értékét át lehetne másolni. A paraméterváltozó vagy egy pointer, amelyik a memóriafoglalás címét kapja értékül (cím szerinti paraméterátadás), vagy egy úgynevezett referenciaváltozó, amelyik az aktuális paraméterként megadott változó memóriafoglalását használja, mintha annak a változónak egy másodlagos (alias) neve lenne (hivatkozás szerinti paraméterátadás). Érték-eredmény szerinti paraméterátadás Az érték szerinti paraméterátadást kombináljuk azzal, hogy amikor az alprogram befejeződik, akkor az aktuális paraméterként megadott változóba átmásolódik a megfeleltetett paraméterváltozó értéke. Ettől eltekintve a paraméterváltozó teljesen különálló változóként, az alprogram lokális változójaként használható. 207

208 Név szerinti paraméterátadás A paraméterváltozó egy sablon, amely helyébe egy-egy konkrét hívás ismeretében szövegszerűen másolódik be a megfeleltetett aktuális paraméter. Hivatkozás szerinti paraméterátadás történik az alábbi példában. Az x paraméterváltozó nem egy önálló új változó, hanem egy úgynevezett referenciaváltozó, amelyik azonos memória területet használ a hívás helyén feltüntetett a v változóval. Bármit változtat az alprogram az x változóban az a változás v változóban is megjelenik. Az x változó valójában egy bemenő- és eredmény paraméterváltozó egyben, hiszen az előbb leírtak szerint már a létrejöttekor (híváskor) látható x változóban a v értéke, és a hívó programba történő visszatérés után a v változóban a x értéke. hívás: int v = 23; fv(v); hívott: void fv(int &x) { Általános szabályként alkalmazzuk a C++ nyelvben azt, hogy a kizárólag bemenő paraméterváltozókhoz az érték szerinti paraméterátadást, az eredmény paraméterváltozókhoz pedig a hivatkozás szerinti paraméterátadást használjuk. Hatékonysági okból azonban célszerű az összetett típusú bemenő paraméterváltozóknál is a hivatkozás szerinti paraméterátadást alkalmazni. Az érték szerinti paraméterátadás esetén ugyanis mindig lemásolódik az átadott paraméter értéke, hiszen ilyenkor a paraméterváltozó önálló memóriafoglalással rendelkezik. A memóriafoglalás ilyen duplázása nem okoz gondot egy egyszerű (tehát néhány bájtot elfoglaló) adat esetén, de összetett típusú adatoknál ez már nagy veszteséget jelenthet. Ezt elkerülendő, ilyenkor is hivatkozás szerinti paraméterátadást alkalmazunk. Ennek a megoldásnak a szépséghibája az, hogy egy ilyen paraméterváltozó egyben eredmény paraméterváltozó is lesz, tehát képes a neki átadott paramétert megváltoztatni. Hogy ez mégse történjen meg (nem elégszünk meg a programozó isten bizony nem változtatom meg ígéretével) a const kulcsszót írjuk a paraméterváltozó típusa elé. Ennek eredményeként fordítási hibát fogunk kapni, ha az alprogramban meg akarnánk változtatni az ilyen paraméterváltozó értékét. Összefoglalva tehát, 208

209 az összetett típusú kizárólag bemenő paraméterváltozókat konstans referenciaváltozóként deklaráljuk. (Külön szabály vonatkozik az eredeti C++os tömb típusú paraméterekre, de mivel helyettük a vector típust használjuk, ezért erre itt nem térünk ki.) A C++ nyelvben minden alprogramot függvénynek nevezünk. A függvény fejében legelőször a visszatérési érték típusát kell megadni, majd azt követően a függvény nevét, azután a paraméterlistáját. A függvénynek egyetlen, de tetszőleges típusú visszatérési értéke lehet. Ha több értéket kell visszaadni, akkor azokat összefoghatjuk egy tömbbe vagy egy rekordba (egy struktúrába), és így formailag egyetlen értéket adunk vissza. A függvény törzse kapcsos zárójelek közé kerül. A visszatérési értéket a függvény kódjában egy kitüntetett utasításban (úgynevezett return utasításban) elhelyezett kifejezés formájában kell leírni. Ennek az utasításnak a végrehajtása a függvény befejeződését vonja maga után. Az eljárásokat úgy jelöljük, hogy a visszatérési típus helyére a void kulcsszót írjuk. Ilyenkor nem kell return utasítást használni, de üres return utasítással kikényszeríthetjük az eljárás befejeződését. Tehát C++ nyelven az eljárás egy visszatérési érték nélküli függvény. Mi történik egy alprogram hívásakor? Amikor egy alprogram hívására sor kerül, annak érdekében, hogy majd a meghívott alprogram befejeződésekor vissza lehessen térni a hívó programrészhez, számos, a hívó programrész működésével kapcsolatos információ elmentésre kerül. A hívó programrész lokális változói már a hívás előtt a verem memóriában (STACK) vannak. A híváskor ide másolódnak a központi egység regisztereinek értékei is, mint például az utasítás számláló (PC), utasítás regiszter (IR), akkumulátor (AC) stb. Csak ezeket követően kerülnek a verem memóriába a hívott alprogrammal kapcsolatos adatok, például ekkor foglalnak itt automatikusan helyet a meghívott alprogram lokális változói, köztük a paraméterváltozói is, és a bemenő paraméterváltozók megkapják kezdőértéküket A return utasítás hatására a verem memóriából törlődnek a hívott alprogrammal kapcsolatos adatok miután a kimenő paraméterváltozók 209

210 érékei és az esetleges visszatérési érték a hívó program megfelelő helyére került, a verem memóriából visszatöltődnek a korábban elmentett értékek a regiszterekbe, hogy a hívó programrész folytatódhasson. A bemutatott programjainkban már eddig is használtunk függvényt, hiszen a main() egy olyan alprogram, amelyet az operációs rendszer hív meg, befejeződésekor oda tér vissza a vezérlés. A main függvénynek lehet bemenő adatokat is adni, amelyeket például egy parancssorból történő futtatás kezdeményezésekor adhatunk meg, és a visszatérési értékét is le lehet kezelni. 210

211 13. Feladat: Faktoriális Számoljuk ki egy adott természetes szám faktoriálisát! Specifikáció A feladat specifikációjában a faktoriális számításnak az eredeti definícióját adjuk meg, nevezetesen, hogy n! = 2 * 3 * * n. A 0 és az 1 faktoriálisa definíció szerint 1. A = ( n : N, f : N ) Ef = ( n=n ) Uf = ( n=n f = ) Absztrakt program A feladat visszavezethető az összegzés programozási tételére úgy, hogy itt az összeadás helyett az összeszorzás műveletét kell használnunk, amelynek nulleleme az 1, az m..n intervallum helyett 2..n intervallum szerepel és az általános f(i) kifejezést most az i helyettesíti, az s eredményváltozó pedig itt az f. f := 1 i = 2.. n f := f * i Implementálás A megoldó programban két alprogramot (függvényt) vezetünk be. Ez egyik a faktoriális kiszámításáért felel (Factorial), amely megkap bemenő adatként egy természetes számot és visszaadja annak faktoriálisát. A másik függvény (ReadNat) egy természetes számot olvas be és adja vissza, de meg kell neki adni bemenetként azt a szöveget, amelyet a beolvasás előtt akarunk kiírni a konzolablakba, illetve azt a szöveget, amit hibás adat beírása esetén kívánunk majd megjeleníteni. 211

212 A program kerete A main függvény beolvas a billentyűzetről egy természetes számot, kiszámolja a faktoriálisát, majd kiírja az eredményt a konzol ablakba. int n = ReadNat("Kérem a számot: ", int f = Factorial(n); cout << "Faktorialis = " << f; "Természetes számot kérek!"); Ezt a kódrészletet még tömörebben, egyetlen utasítással is le lehet írni, mint ahogy azt az alábbi programkódban láthatjuk. A végső megoldásban lehetővé tesszük, hogy a programot ciklikusan újra és újra végre lehessen hajtani mindaddig, amíg a felhasználó ezt kéri. #include <iostream> using namespace std; int ReadNat(const string &msg, int Factorial(int n); const string &errmsg); int main() { cout << "Egy szám faktoriálisát számolom?\n"; string tmp; char ch = 'i'; do{ 212

213 cout << "Faktoriális = " << Factorial( ReadNat("Kérem a számot: ", "Természetes számot kérek!" )); cout << "\n\nfolytatja? (I/N)"; cin >> ch; getline(cin,tmp); while(ch!= 'n' && ch!= 'N'); return 0; Az alprogramok közötti hívási kapcsolatot teszi szemléletessé az alábbi ábra. A nyilak jelzik, hogy melyik alprogram hívja a másikat. A nyilak mellé oda lehetne még írni az adatforgalmat is: milyen bemenő adat adódik át híváskor, és milyen eredményt szolgáltat a meghívott alprogram. main() ReadNat() Factorial() 6-3. ábra. Alprogramok hívási láncai Mindhárom alprogram ugyanabban a forrásállományban (legyen ennek a neve main.cpp) foglal majd helyet. Ebben elől az iostream csomag bemásolása és a using namspace std utasítás áll, ezután a main függvényből meghívott függvények deklarációi, majd a main függvény, és végül a másik két függvény definíciója. 213

214 Absztrakt program kódolása Az absztrakt program alapján a Factorial() függvény törzsét készíthetjük el, annak kódja a struktogramm szó szerinti lefordításának eredménye. int Factorial(int n) { int f = 1; for(int i = 2; i<=n; ++i){ f = f * i; return f; Az absztrakt program három változót használ. Az n változó a Factorial függvény bemenő paraméterváltozója lesz. Ez lokális a függvényre nézve, de kezdőértékét a hívás helyéről kapja. Ehhez érték szerinti paraméterátadást alkalmazunk. A Factorial függvény az absztrakt program eredményét visszatérési értékként juttatja el a hívás helyére, ezért az eredmény változót a függvényben lokális változóként definiáljuk és a program végén a return utasítással adjuk vissza az értékét. Az absztrakt program lokális i változója a C++ függvénynek is lokális változója lesz, ráadásul a láthatóságát a ciklusra korlátozzuk. Elvileg elkészíthetnénk a Factorial függvényt úgy is, hogy az eredményt paraméterváltozón keresztül adja vissza. Ehhez az f-et hivatkozás szerinti paraméterváltozóként kellene definiálni az n bemenő paraméterváltozó mellett. Ilyenkor nincs szükség a return utasításra hiszen a függvény visszatérési típusa void. Ennek a megoldásnak a hátránya az, hogy ezt eljárásszerűen kellene a main függvényből meghívni: Factorial(n, f); 214

215 Beolvasás A beolvasáshoz használt függvény két paraméterváltozóval rendelkezik. Mindkettőt kizárólag bemenő paraméterváltozóként fogjuk használni, de mivel összetett (sztring) típusúak, ezért nem érték szerinti, hanem konstans hivatkozás szerinti paraméterátadással oldjuk meg a kezdeti értékadásukat. int ReadNat(const string &msg, const string &errmsg) { int n; bool error = true; do{ cout << msg; cin >> n; if(error = cin.fail() n<0){ cout << errmsg << endl; cin.clear(); string tmp; getline(cin,tmp); while(error); return n; A függvény törzsében a korábban már ismertetett technikával próbálunk egy billentyűzetről egy nem negatív számot beolvasni. Ha a megadott adat nem szám vagy negatív, hibajelzést adunk, és újra megkíséreljük a beolvasást. Ennél fogva ennek a függvénynek csak akkor fejeződik be a végrehajtása, ha sikerül egy természetes számot beolvasni. 215

216 Tesztelés A teszteseteket alprogramonként, azaz modulonként adjuk meg. A Factorial()alprogram az absztrakt program kódja, ezért ennek fekete doboz tesztesetei a feladat specifikációjából nyerhető érvényes tesztesetek. A kód egyszerűsége miatt ezek egyben a fehér doboz tesztelés szempontjainak is megfelelnek: 1. Szélsőséges adatok ( 0! = 1, 1! = 1, 2! = 2) 2. Általános adat (4! = 24) 3. Skálázás: legkisebb olyan szám keresése, amelyik faktoriálisa már nem ábrázolható az int típussal. (Próbáljuk meg a long int vagy a double típust használni az eredmény változóra. Látni fogjuk, hogy ezek sem kielégítőek, valójában egy igazán nagy számokat ábrázolni képes egész szám típusra lenne itt szükség, de ilyen a C++ nyelvben nincs. Mi készíthetnénk ilyet, de ennek technikájáról a könyv harmadik részében lesz majd csak szó.) A ReadNat() feladata egy természetes szám beolvasása, de különösen az ebből a szempontból érvénytelen adatok lekezelése. A fehér doboz teszteléséhez többször kell egymás után érvénytelen adatot beírni. 1. Természetes számok (0, 1 és ennél nagyobb) beolvasása 2. Negatív egész számok esete. 3. Nem egész szám esete. A main() fekete doboz tesztelésénél a feladat érvénytelen tesztadatait kell kipróbálni, de ez itt már nem vezet újabb tesztesetekhez. Fehér doboz tesztesetekkel a do-while ciklust kell lefedni (a ciklusmag egyszer illetve többször fusson le). 216

217 Teljes program #include <iostream> #include <string> using namespace std; int ReadNat(const string &msg, const string &errmsg); int Factorial(int n); int main() { cout << "Egy szám faktoriálisát számolom?\n"; string tmp; char ch = 'i'; do{ cout << "Faktoriális = " << Factorial( ReadNat("Kérem a számot: ", "Természetes számot kérek!" )); cout << "\n\nfolytatja? (I/N)"; cin >> ch; getline(cin,tmp); while(ch!= 'n' && ch!= 'N'); return 0; int Factorial(int n) 217

218 { int f = 1; for(int i = 2; i<=n; ++i){ f = f * i; return f; int ReadNat(const string &msg, const string &errmsg) { int n; bool error = true; string tmp; do{ cout << msg; cin >> n; if(error = cin.fail() n<0){ cout << errmsg << endl; cin.clear(); string tmp; getline(cin,tmp); while(error); return n; 14. Feladat: Adott számmal osztható számok 218

219 Válogassuk ki egy egész számokat tartalmazó tömb adott k számmal osztható elemeit egy sorozatba! Specifikáció A feladat egy egész számokból álló sorozatot kíván összefűzni a tömb k-val osztható elemeiből. A = ( t : Z n, k : Z, s : Z * ) Ef = ( t=t k=k k 0 ) Uf = ( t=t k=k s = ) Absztrakt program A feladat visszavezethető az összegzés programozási tételére úgy, hogy itt az összeadás helyett az összefűzés műveletét kell használnunk, amelynek nulleleme az üres sorozat (< >), az m..n intervallum helyett 1..n intervallum szerepel és az f(i) most egy feltételes kifejezéssel helyettesíthető: ha k t[i] akkor <t[i]> különben < >. s := < > i = 1.. n k t[i] s := s <t[i]> SKIP Implementálás A tömböt egy olyan szöveges állományból töltjük fel, amelynek első eleme a tömb mérete (ez természetes szám), azt követően pedig elválasztó jelekkel (szóköz, tabulátor jel, sorvége jel) határolva a tömb elemei (egész számok) következnek. Feltesszük, hogy az állomány formája helyes, azt nem kell ellenőriznünk. 219

220 A tömböt vector<int> típussal fogjuk ábrázolni, amely 0-tól kezdődően indexelődik, ezért az absztrakt program ciklusváltozója a 0..n-1 intervallumot fogja befutni. Az eredmény sorozatot a cout adatfolyammal valósítjuk meg, így a << operátor helyettesíti a specifikációban használt operátort. Ennek következményeként az eredmény sorozathoz hozzáfűzött elemek közvetlenül a konzolablakban jelennek majd meg. Bevezetünk három függvényt. Az egyikben az állomány megnyitására és a tömb feltöltésére (FillVector) kerül sor, a másikban egy nem-nulla egész szám beolvasására (ReadNotNullInt), a harmadikban (Selecting) az absztrakt programot kódoljuk. A program kerete Egy forrásállományban (legyen ennek a neve main.cpp) helyezzük el a main függvényt, amelyet megelőz az iostream, az fstream és a vector csomag bemásolása, a using namespace std utasítás, majd a függvények deklarációi, amelyek definíciója a main függvény után helyezkedik el. #include <iostream> #include <fstream> #include <vector> #include <string> using namespace std; vector<int> FillVector(); int ReadNotNullInt(const string &msg, const string &errmsg); void Selecting(const vector<int> &t, int k); 220

221 int main() { cout << "Adott számmal osztható számok\n"; vector<int> t = FillVector(); int k = ReadNotNullInt("\nKérem az osztót: ", "Egész számot kérek!\n"); cout << "Eredmény: "; Selecting(t,k); return 0; A main függvény kódjából jól látható, hogy a program mely függvényeket hívja meg. FillVector() main() ReadNotNullInt() Selecting() 6-4. ábra. Alprogramok hívási láncai A main egy tömböt kap a FillVector() függvénytől, egy egész számot a ReadNotNullInt()-től. A Selecting() megkapja a tömböt, a nullától különböző számot, és kiírja a szabványos kimenetre a tömb adott számmal osztható elemeit. Tömb beolvasása 221

222 A FillVector() függvény egy vector<int> típusú objektumot ad vissza, amely előállítását egy szöveges állomány adatai alapján végzi. Először megnyitja a megfelelő szöveges állományt olvasásra, ehhez beolvassa az állomány nevét, és ellenőrizzük, hogy azt jól adták-e meg. vector<int> FillVector() { ifstream f; bool error = true; string str; do{ cout << "Fájl neve: "; cin >> str; f.open( str.c_str() ); if ( error = f.fail() ){ cout << "Nincs ilyen fájl!" << endl; f.clear(); while(error); Az állományban elhelyezett első számadat a létrehozandó tömb hosszát tartalmazza, ezért ezt beolvasva definiálhatjuk a tömböt. Ezt követően töltjük fel a tömböt az állományban található egész számokkal, majd lezárjuk az állományt. int n; f >> n; vector<int> t(n); 222

223 for(int i=0; i<n; ++i){ f >> t[i]; f.close(); return t; Nullától különböző egész szám beolvasása Ez tulajdonképpen az előző feladatban bemutatott ReadNat() módosítása, ahol a beolvasott egész szám vizsgálatánál hibás adatnak nem az n<0 feltételt kielégítő egész számot, hanem a 0==n feltételnek megfelelő számot tekintjük. int ReadNotNullInt(const string &msg, const string &errmsg) { int n; bool error = true; string tmp; do{ cout << msg; cin >> n; if(error = cin.fail() 0 == n){ cout << errmsg << endl; 223

224 cin.clear(); getline(cin,tmp); while(error); return n; Kiválogatás A megoldás harmadik része az absztrakt program kódját tartalmazó alprogram, amelyet eljárásszerűen hívunk meg. Két, kizárólag bemenő paraméterváltozója lesz: az egyik a bemenő adatokat tartalmazó tömb, a másik az oszthatóság vizsgálatához megadott nem nulla egész szám. Az elsőnek összetett típusa van, ezért annak az értékét konstans referenciaként, a másodiknak a típusa egyszerű, azt érték szerint fogjuk átvenni. void Selecting(const vector<int> &t, int k) { for(int i = 0; i<(int)t.size(); ++i){ if(t[i]%k == 0) cout << t[i] << " "; Tesztelés A Selecting() teszteléséhez a feladat érvényes fekete doboz teszteseteit használjuk, külön fehér doboz tesztesetekre nincs szükség: 224

225 1. Üres tömb esete. (válasz: üres sorozat) 2. Tetszőleges tömb, az osztó 1. (válasz: tömb minden eleme) 3. Egy elemű tömb, az osztó ez az elem. (válasz: ez az elem) 4. Egy elemű tömb, az osztó ez az elem+1. (válasz: üres sorozat) 5. Tetszőleges tömb, az osztó a legnagyobb elem+1. (válasz: üres sorozat) 6. Több elemű tömb, csak az első eleme osztható az adott számmal. 7. Több elemű tömb, csak az utolsó eleme osztható az adott számmal. 8. Negatív osztó, negatív elemek a tömbben. A ReadNotNull() az érvényes osztó előállítását végzi. Fekete doboz tesztelése az érvénytelen adatokkal történik: 1. Nulla osztó esete. 2. Nem számként megadott osztó esete. A ReadNotNull() fehérdoboz teszteléséhez többször kell egymás után rossz adatot megadni: 1. Egymás után több rossz próbálkozás esete. A FillVector() garantáltan érvényes adatokat feltételez, ezért csak fehér doboz tesztesetek tartoznak hozzá: 1. Nem létező állománynév esete. 2. Egymás után több rossz állománynév beírása. 3. Különféle elválasztó jelek alkalmazása a szöveges állományban. 4. Nulla elemű, egy elemű, és egy sok elemű tömb előállítása. A main() fekete doboz tesztelésénél a feladat érvénytelen tesztadatait kell csak kipróbálni, de ez itt már nem vezet újabb tesztesetekhez. 225

226 Teljes program #include <iostream> #include <fstream> #include <vector> #include <string> using namespace std; vector<int> FillVector(); int ReadNotNullInt(const string &msg, const string &errmsg); void Selecting(const vector<int> &t, int k); int main() { cout << "Adott számmal osztható számok\n"; vector<int> t = FillVector(); int k = ReadNotNullInt("\nKérem az osztót: ", "Egész számot kérek!\n"); cout << "Eredmény: "; Selecting(t,k); return 0; void Selecting(const vector<int> &t, int k) 226

227 { for(int i = 0; i<(int)t.size(); ++i){ if(t[i]%k == 0) cout << t[i] << " "; int ReadNotNullInt(const string &msg, const string &errmsg) { int n; bool error = true; string tmp; do{ cout << msg; cin >> n; if(error = cin.fail() 0 == n){ cout << errmsg << endl; cin.clear(); getline(cin,tmp); while(error); return n; vector<int> FillVector() 227

228 { ifstream f; bool error = true; string str; do{ cout << "Fájl neve: "; cin >> str; f.open( str.c_str() ); if ( error = f.fail() ){ cout << "Nincs ilyen fájl!" << endl; f.clear(); while (error); int n; f >> n; vector<int> t(n); for(int i=0; i<n; ++i){ f >> t[i]; f.close(); return t; 228

229 229

230 15. Feladat: Páros számok darabszáma Számoljuk meg egy egész számokat tartalmazó tömb páros elemeit! Specifikáció A feladat egy számlálás. A = ( t : Z n, s : N ) Ef = ( t=t ) n Uf = ( t=t s = 1 ) i 1 2 t[ i] Absztrakt program A feladat visszavezethető a számlálás programozási tételére úgy, hogy itt az m..n intervallum helyett 1..n intervallum szerepel és a (i) feltétel a 2 t[i]. s := 0 i = 1.. n 2 t[i] s := s + 1 SKIP Implementálás A tömböt egy olyan szöveges állományból töltjük fel, amely nem tartalmazza explicit módon a tömb elemeinek számát, elemei elválasztó jelekkel (szóköz, tabulátor jel, sorvége jel) határolt egész számok, a legutolsó sor végén is van sorvége jel. Feltesszük, hogy az állomány tartalma helyes, ezért azt nem kell ellenőriznünk. 230

231 A tömböt vector<int> típussal fogjuk ábrázolni, amely 0-tól kezdődően indexelődik, ezért az absztrakt program ciklusváltozója a 0..n-1 intervallumot fogja befutni. Bevezetünk két függvényt is. Az egyiket az állomány megnyitására és a tömb feltöltésére (FillVector), a másikat az absztrakt programot kódolására használjuk. main() FillVector() Count() 6-5. ábra. Alprogramok hívási láncai A program kerete Egy forrásállomány (legyen ennek a neve main.cpp) az előző két feladatban alkalmazott szerkezet szerint épül fel. A main függvény deklarálja a tömböt, meghívja a FillVector()-t annak feltöltéséhez, majd a Count()-ot az eredmény kiszámolásához. #include <iostream> #include <fstream> #include <vector> #include <string> using namespace std; void FillVector(vector<int> &t); int Count(const vector<int> &t); 231

232 int main() { cout << "Megszámoljuk a páros számokat.\n"; vector<int> t; FillVector(t); cout << "A páros elemek száma: " << Count(t); return 0; Absztrakt program kódolása Az absztrakt program kódja alkotja a Count() törzsét. int Count(const vector<int> &t) { int s = 0; for(int i = 0; i<(int)t.size(); ++i){ if(t[i]%2 == 0) ++s; return s; 232

233 Tömb beolvasása Ez a kódrész hasonlít az előző feladatban is használt tömb feltöltéshez. Egy ponton tér el csak attól: most ugyanis azt tételezzük fel a bemenő adatokat tartalmazó szöveges állományról, hogy nem tartalmazza az adatok számát, így a tömböt egy fájlvégéig tartó olvasással fokozatosan nyújtjuk megfelelő méretűre. Ezért a korábbi for ciklus helyett itt egy while ciklus szerepel, amely az előre olvasási technikát valósítja meg. void FillVector(vector<int> &t) { ifstream f; bool error = true; string str; do{ cout << "Fájl neve: "; cin >> str; f.open( str.c_str() ); if ( error = f.fail() ){ cout << "Nincs ilyen nevű fájl" << endl; f.clear(); while (error); int e; f >> e; while(!f.eof()){ 233

234 t.push_back(e); f >> e; f.close(); Tesztelés A Count()fekete doboz teszteléséhez a feladat érvényes teszteseteit használjuk. Külön fehér doboz tesztelésre itt nincs szükség: 1. Üres tömb esete. (válasz: 0 darab páros szám) 2. Egyetlen páros szám a tömbben. (válasz: 1 darab páros szám) 3. Egyetlen páratlan szám a tömbben. (válasz: 0 darab páros szám) 4. Tetszőleges tömb, negatív számokkal. 5. Több elemű tömb, csak az első eleme páros. (válasz: 1 darab páros szám) 6. Több elemű tömb, csak az utolsó eleme páros. (válasz: 1 darab páros szám) A FillVector()egy érvényes bemeneti tömböt állít elő, amely a feladat szövege szerint garantált. Nincsenek tehát a feladatnak vizsgálandó érvénytelen tesztesetei. Fehér doboz tesztesetek azonban vannak: 1. Nem létező állománynév esete. 2. Egymás után több rossz állománynév beírása. 3. Különféle elválasztó jelek alkalmazása a szöveges állományban. 4. Nulla, egy, sok elemű tömb előállítása. A main() fekete doboz tesztelésénél a feladat érvénytelen tesztadatait kell kipróbálni, de ez itt már nem vezet újabb tesztesetekhez. 234

235 Teljes program #include <iostream> #include <fstream> #include <vector> #include <string> using namespace std; void FillVector(vector<int> &t); int Count(const vector<int> &t); int main() { cout << "Megszámoljuk a páros számokat.\n"; vector<int> t; FillVector(t); cout << "A páros elemek száma: " << Count(t); return 0; int Count(const vector<int> &t) { int s = 0; for(int i = 0; i<(int)t.size(); ++i){ if(t[i]%2 == 0) ++s; 235

236 return s; void FillVector(vector<int> &t) { ifstream f; bool error = true; string str; do{ cout << "Fájl neve: "; cin >> str; f.open( str.c_str() ); if ( error = f.fail() ){ cout << "Nincs ilyen nevű fájl" << endl; f.clear(); while (error); int e; f >> e; while(!f.eof()){ t.push_back(e); f >> e; f.close(); C++ kislexikon 236

237 eljárás-alprogram void Name() { eljárás hívása függvény-alprogram Name(); int Name() { int b = ; return 5*b; függvény hívása kizárólag bemenő paraméterváltozók int a = Name(); void Name(const string &str, int k) { bemenő és eredményparaméterváltozó void Name(int &k, bool &l) { vegyes int Name(int k, bool &l) { l = true; 237

238 return 5*k; 238

239 7. Programozási tételek implementálása A visszavezetésre (lásd első kötet) támaszkodó programtervezés eredménye egy olyan program, amelyben önálló egységeket alkotnak a programozási tételekből származtatott programrészek. Egy-egy ilyen programrész alprogramba szervezhető, és a kódja szabványosítható, azaz kis odafigyeléssel elérhető, hogy ugyanannak a programozási tételnek a kódja mindig ugyanúgy nézzen ki. Implementációs stratégia A visszavezetés technikájával tervezett programok különféle programozási tételek mintája szerint épülnek fel, és jól elkülöníthetők bennük az egyes tételekből származtatott részek. Mivel ezek a részek egy-egy részfeladatot oldanak meg, ezért mindenképpen érdemes a kódjaikat külön alprogramokban megadni. Ennek megfelelően a kódban egy programozási tétel egy alprogram lesz. Mivel egy programozási tétel egyetlen ciklust tartalmaz, ez az implementációs stratégia lényegében az egy alprogram-egy ciklus elvét is kimondja. Természetesen ezt az elvet rugalmasan kell kezelnünk, mert például egy mátrixban történő számlálásnál, amelynél az elemek bejárását egy dupla ciklussal végezzük, nem lenne célszerű a belső ciklust külön alprogramba tenni. Lehetnek olyan esetek is, amikor több tételnyi kódot tartalmaz egyetlen függvény, amely esetleg kiegészül más funkciójú kódrészekkel, például egy beolvasással. A programozási tételekből származtatott algoritmusok kódolására érdemes kialakítani egy olyan stílust, amelyet programjainkban egyfajta szabványként alkalmazunk. Amilyen következetesen járunk el a visszavezetés tervezési módszerének alkalmazásában, éppen olyan következetesen ragaszkodjunk a kialakított kódolási stílushoz. Miért is kellene, mondjuk egy számlálást mindig másképpen kódolni? Vizsgáljuk meg a választott programozási nyelv nyújtotta lehetőségeket, válasszuk ki a lehető legáltalánosabb, futási idejében leggyorsabb kódot, és azt minden körülmények között következetesen, mint egy kódmintát, alkalmazzuk. 239

240 Az ilyen önként vállalt vagy a munkahelyünkön kialakított megállapodására épülő szabvány alkalmazása a kezdeti nehézségeket leszámítva meggyorsítja a kódolást, jelentősen csökkenti a kódolás során elkövetett hibákat. A kódunk áttekinthetővé, mások számára is jól olvashatóvá válik. Ez utóbbi szempont miatt célszerű, ha nem egyedi stílust alakítunk ki, hanem a programozó szakmában kialakított sémákat utánozzuk. Összegzés Számlálás Maximum kiválasztás Lineáris keresés Kiválasztás Feltételes maximumkeresés 7-1. ábra. Leggyakrabban alkalmazott programozási tételek A programozási tételek kódolásának szabványosítása nemcsak azt jelenti, hogy ugyanazt a tételt mindig ugyanúgy kódoljuk, hanem azt is, hogy a hasonló tételeket, hasonló módon. Például az intervallum elemeinek bejárására épülő tételek kódjaiban egységesen, ugyanolyan ciklusszervezést alkalmazunk. A programozási tételekből származtatott alprogramok implementálásánál fontos szempont a kód-újrafelhasználás. Ha például a programunkban több helyen is alkalmazzuk a számlálás programozási tételét, akár eltérő paraméterek mellett, akkor megfontolandó, hogy a kódban ne egyetlen számlálást megvalósító alprogramot írjunk-e, amelyet aztán eltérő paraméterekkel hívhatunk meg. Elsősorban a rendelkezésünkre álló nyelvi eszközökön múlik, hogy mennyire mehetünk el az általánosítás irányába. Természetesen ügyeljünk arra, hogy a kód ilyen redundanciáját (hasonló kódrészek ismétlődését) megszüntető összevonások ne növeljék a program futási idejét. Próbáljuk megtalálni a helyes egyensúlyt. 240

241 Bár nem az implementációs stratégiákhoz tartozik, de itt érdemes néhány szót szólni a visszavetetéssel készült programok tesztelési stratégiájáról. Amikor egy programrész valamely intervallumra megfogalmazott programozási tétel mintájára készül, akkor az alábbi (érvényes) teszteset-csoportokat érdemes vizsgálni. Az intervallum tesztelése olyan teszteseteket foglal magába, amikor megvizsgáljuk a megoldó programot üres intervallumra (ennek maximum kiválasztás esetén nincs értelme); egy elemű intervallumra; és olyan adatokkal, amelyekből kiderül, hogy az intervallum eleje is, vége is vizsgálat alá esett. Ez utóbbi egy számlálásnál azt kívánja, hogy a feltétel az intervallum első és utolsó elemére is igaz legyen. Egy maximum kiválasztásnál azt, hogy a maximális elem az első vagy az utolsó helyen jelenjen meg. Lineáris keresésnél azt, hogy a vizsgált tulajdonság az első vagy csak az utolsó helyen legyen igaz. A programozási tétel tesztelése az alkalmazott algoritmus minta specialitásait vizsgálja. Számlálásnál azt, hogy a válasz lehet-e 0, 1 illetve több. Maximum kiválasztásnál azt, hogy megtalálja-e az egyetlen, vagy több közül az egyik maximális elemet. Lineáris keresésnél olyan esetet nézünk, amikor nincs keresett elem, amikor van, és a ha több is van, akkor vajon az elsőt találjuk-e meg. Az elemek típusának tesztelése a programozási tételekben szereplő intervallumon értelmezett függvény értékkészletének illetve az intervallumon értelmezett feltétel kiszámításában résztvevő értékek típusérték halmazának különleges, szélsőséges elemeivel számol. Például ott, ahol egész számokkal van dolgunk, érdemes a nullával, az eggyel, negatív és pozitív számokkal is kipróbálni a programot. Ha sztringekkel dolgozunk, akkor az üres sztring, az ékezetes betűket tartalmazó sztring számít különleges értéknek. Látjuk, hogy a fenti teszt-csoportok mindegyikéhez több teszteset is tartozik. Minden tesztesethez a megnevezésén túl készíteni kell egy tesztadatot, valamint az arra várt helyes választ, és ezekkel a tesztadatokkal kell letesztelni a programunkat. 241

242 Nyelvi elemek Egy programozási tételt sokféleképpen kódolhatunk egy programozási nyelven. C++ nyelven például lehet a ciklust while vagy for utasítással kódolni, eldönthetjük, hogy akarunk-e speciális ugró utasításokat (break, continue) alkalmazni. Az is tőlünk függ, hogy egy alprogramba ágyazott kódrészlet eredménye milyen módon (eredmény-paraméter, visszatérési érték) jusson vissza a hívás helyére. Ezekre a kérdésekre elég egy, lehetőleg általános és hatékony megoldást megadni, és aztán ehhez a megállapodáshoz igazodva kis gyakorlással elérhető legyen, hogy egyazon tételből származó kódot mindig ugyanúgy írjunk le. Például a számlálásra mindkét alábbi változat jó, de eldöntjük, hogy ezen túl következetesen mindig az elsőt fogjuk használni. int s = 0; int s = 0, i = m; for(int i=m; i<=n; ++i){ while( i<=n ){ if( felt(i)) ++s; if( felt(i)) ++s; ++i; A stílus uniformizálásának egyik alapja lehet a számlálós ciklus következetes alkalmazása, mivel a tételek többsége előre meghatározott számú iterációt végez. Ez alól a lineáris keresés és a kiválasztás programozási tétele kivétel, mivel ott a ciklus leállása egy speciális feltételtől függ. Egyébként pont a lineáris keresés az, amelyet meglehetősen sokféleképpen lehet kódolni. Ennek egyik oka az, hogy már a programozási tételben szereplő absztrakt algoritmus leírására is több változat létezik, vannak speciális változatok, de az általános változatnak is több lehetséges leírása. (A mi változatunkat az első kötetben találjuk.) A másik ok az, hogy azokban a programozási nyelvekben (pl. Pascal), ahol a for ciklus kizárólag a számlálós ciklusok kódolására szolgál a lineáris keresést csak elől tesztelős (while) ciklussal tudjuk kódolni. (Hagyjuk figyelmen kívül a ciklusmagból goto segítségével történő kiugrás lehetőségeit.) A C-szerű nyelvekben azonban a 242

243 for utasítás általánosabb, mint egy számlálós ciklus, és ezért ott lehetőség nyílik a korábban említett egységes kódolás érdekében a lineáris keresés kódolásához is for ciklust használni. Mivel azonban a for utasítás igen rugalmas, ezért ezzel is többféle verzió készíthető. Nézzünk két, for utasítást tartalmazó megoldást (ebben a felt(i) egy itt nem meghatározott, a keresés feltételét vizsgáló logikai értékű függvény). Mindkettő a lehető legáltalánosabb megoldást nyújtja, de a jobboldali egy speciális ugró utasítást (break) is tartalmaz. Ennek végrehajtásakor a vezérlés kiugrik a ciklusból az azt követő utasításhoz. bool l = false; bool l = false; int ind; int ind; for(int i=m;!l && i<=n; ++i){ for(ind=m; ind<=n; ++ind){ l = felt(i); if(l= felt(ind)) break; ind = i; A jobboldali változat talán furcsa a strukturált programozáshoz végletekig ragaszkodó programozók számára, de az előnye kézenfekvő: míg baloldali változatban egy külön lokális ciklusváltozó futja be az m..n intervallumot és az esetlegesen megtalált elem egy másik (ind) változóba kerül, addig jobboldali változatban az ind változó egyszerre ciklusváltozó és eredményváltozó is. Ha megszokjuk, hogy kizárólag egy lineáris keresés kódolásánál használjuk a for ciklust a break utasítással együtt a fenti formában, akkor ez a fajta strukturálatlanság egyáltalán nem fog problémát okozni. (Természetesen az ugró utasítások nyakra-főre alkalmazását kerülni kell, azokat csak jól bejáratott szövegkörnyezetben, valamilyen strukturált program rövidített leírásaként szabad használni.) A kiválasztásnál még intervallumról sem beszélhetünk, amelynek elemeit be kellene futni, mégis alkalmazható a kódolásához a for utasítás. Arra kell csak figyelni, hogy a ciklusváltozó itt egyben eredményváltozó is, 243

244 ezért nem a for utasítás hatáskörében kell definiálni azt, hanem még a ciklus előtt: int i; for(i = m;!felt(i); ++i); A feltételes maximumkeresés (egy f(i) függvénykifejezés értékei között keresünk felt(i) feltételt kielégítő legnagyobb értéket) kódjában felhasználhatjuk a C++ nyelv continue ugró utasítását. Ennek hatására a vezérlés a ciklusmag többi részét átugorja, és a ciklusfeltétel kiértékelésénél folytatódik. Olyan ez, mintha a ciklusmag többi része a continue-t tartalmazó if ágnak az else ágában lenne. Ha úgy tekintünk a continue szerepére, hogy az egy elágazás egyszerűsített formájú leírására szolgál, mert segítségével nem kell túlságosan egymásba ágyazni a ciklusmag elágazásait, akkor ezt már a strukturált programozás hívei is használhatják. bool l = false; for(int i=0; i<n; ++i){ if (!felt(i)) continue; if(l){ if(f(i)>max){ else { max = f(i); ind = i; l = true; max = f(i); ind = i; Egy másik, ugró utasítást nem tartalmazó feltételes maximumkeresés változat az alábbi: 244

245 bool l = false; for(int i=0; i<n; ++i){ if (felt(i) && l){ if(f(i)>max){ max = f(i); ind = i; else if (felt(i) &&!l){ { l = true; max = f(i); ind = i; Szabványosítást igényel egy-egy programozási tételt tartalmazó alprogram paraméterezésének módja is. Első pillantásra célravezetőnek tűnik, ha az eredményt visszatérési értékként adjuk vissza, de ebben erős korlátozást jelenthet a választott programozási nyelv. Szerencsére a C++ nyelv ebből a szempontból is rugalmas: igaz, hogy csak egy értéket tud egy függvény visszaadni, de annak tetszőleges lehet a típusa. Ha egyszerre több eredménye is van egy függvénynek, akkor azokat egy struktúrába összefogva egy összetett értékként tudjuk visszaadni a hívás helyére. Például a feltételes maximumkeresésnél az alábbi megoldást választhatjuk (Konkrét esetben a Value helyére majd a vizsgált elemek olyan típusát kell írni, amelyre értelmezettek az összehasonlító operátorok.) struct result{ bool l; // van-e keresett elem int ind; // legnagyobb keresett elem indexe Value max; // legnagyobb keresett elem ; 245

246 result feltmaxker( ); Mégsem ezt tartjuk a legjobb megoldásnak. Ha több eredménye is van egy tételnek, amelyek között van egy logikai érték (ilyen a feltételes maximumkeresés és a lineáris keresés), akkor szerencsésebb, ha csak a logikai értéket adja vissza visszatérési értékként az alprogram, a többi eredményt pedig eredmény paraméterváltozó segítségével. Ennek a használata kényelmesebb, mint az előbb bemutatott forma (ezt a továbbiakban érintett feladatok megoldása támasztja majd alá), ráadásul a korlátozottabb lehetőségeket biztosító nyelvek (pl. Pascal) esetében is alkalmazható. bool feltmaxker(, int &ind, Value &max); bool linker(,int &ind); 246

247 16. Feladat: Legnagyobb osztó Határozzuk meg az egynél nagyobb természetes szám önmagától különböző legnagyobb osztóját. Specifikáció A feladat könnyen megoldható, ha a megadott szám felétől elindulunk lefelé az egész számokon, és az első olyat keressük, amelyik osztja a megadott számot. Ilyen osztó biztos van, ha más nem, akkor az 1. Ez tehát egy biztosan fog találni jellegű keresés, azaz egy kiválasztás, amelyet nem a szokásos módon, hanem a számegyenesen lefelé haladva kell alkalmaznunk. A = ( n : N, d : N ) Ef = ( n=n n>1 ) Uf = ( n=n d select d n ) d n / 2 A feladat másképpen is megoldható. Megkereshetjük a 2.. intervallumban az n szám legkisebb osztóját. Ha ilyen van (legyen ez a d), akkor a legnagyobb önmagától különböző osztó az n/d lesz. Ha nincs ilyen, akkor n prím szám, és a legnagyobb önmagától különböző osztója az 1. Uf = ( n=n n l, k search k n k 2 (l d = n/k) ( l d = 1) ) Absztrakt program A feladatot az első esetben a kiválasztás programozási tételére vezethetjük vissza, de úgy, hogy itt a ciklusmagban csökkenteni kell az eredmény változót, nem növelni, hiszen a keresést a számegyenesen az n/2-től indulva balra kell végezni. 247

248 d := n/2 d := d - 1 A második esetben a lineáris keresés programozási tételére vezethetjük vissza a megoldásnak azt a részét, amelyik a legkisebb valódi osztót (k) keresi. Ezt követően még egy elágazásra is szükség van, amely ha a keresés eredményes volt, akkor n/k alakban kiszámolja a legnagyobb n-től különböző osztót, különben 1-ként adja meg azt.,i := hamis,2 i := k := i i := i + 1 l d := n/k d := 1 Implementálás A program három részre tagolódik: az adott szám beolvasására, a legnagyobb nem triviális osztó keresésére és az eredmény kiírására. Ezt a három lépést ciklikusan, újra és újra végre lehet majd hajtatni. A main függvényen kívül két függvényt vezetünk be. Az egyik (ReadInt) egész számok biztonságos beolvasását végzi majd, amelynek meg lehet paraméterként adni azt, hogy csak az egynél nagyobb egész számokat fogadja el. A másik a legnagyobb osztó kereséséért (Divisor) felel. A tervezett ReadInt()-hez hasonló függvényekkel már találkoztunk. (Lásd előző fejezet ReadNat() vagy ReadNotNullInt() függvényeit.) 248

249 Ezeket most úgy általánosítjuk, hogy speciális paramétereként meg lehessen neki adni egy olyan ellenőrző függvényt, amely egész számot vár bemenetként és egy logikai értéket ad vissza. Ez most a GreaterThanOne() lesz, amelyik eldönti egy egész számról, hogy az 1-nél nagyobb-e. Ha ez a feltétel nem teljesül, akkor a beolvasás a megadott számot nem fogadja el. A Divisor() függvény bemenő adatként kap egy 1-nél nagyobb természetes számot, visszatérési értékként pedig ennek önmagától különböző legnagyobb osztóját várjuk tőle. Függvények hívási láncolata Jó áttekintést ad a programról az abban használt függvények hívási kapcsolatrendszere. Ebből kiolvasható, hogy egy függvény melyik másik függvényt hívja meg. Ezeket a hívási láncokat mutatja az alábbi ábra. main() ReadInt() Divisor() GreaterThanOne() 7-2. ábra. Alprogramok hívási láncai A hívási láncok mentén adatcsere zajlik az egyes függvények között. Mivel globális változót nem használunk, a függvények és hívásaik paraméterlistáiból könnyen kiolvasható, hogy milyen adatok adódnák át a híváskor, és milyen adat kerül vissza a hívás helyére. A main() egy 1-nél nagyobb egész számot kap a ReadInt()-től. A ReadInt() ezt a számot előzőleg még megvizsgáltatja a GreaterThanOne() függvénnyel úgy, hogy odaadja neki és az egy logikai értékkel jelzi vissza annak helyességét. A GreaterThanOne() függvényt a main() adja át a ReadInt()-nek. A Divisor() megkapja a main()-től az ellenőrzött egész számot és visszaadja annak legnagyobb önmagától különböző osztóját. 249

250 250

251 Program keret A program main függvény előtt találjuk a programban használt függvények deklarációit. Az All() függvény szerepére később mutatunk rá. #include <iostream> using namespace std; bool GreaterThanOne(int n); bool All(int n); int ReadInt(const string &msg, const string &errmsg, bool check(int) = All)); int Divisor(int n); A program main függvénye tartalmazza a futtatás végtelenítését biztosító do-while utasítást, amelynek a törzsében a szám beolvasását végző ReadInt() függvény meghívására és a számolást végző Divisor() függvény eredményének kiírására kerül sor. int main() { cout << "Legnagyobb osztó!\n"; char ch; string tmp; do{ int n = ReadInt("A szám: ", "1-nél nagyobb szám kell!", 251

252 GreaterThanOne); cout << "Osztó: " << Divisor(n) << endl; cout << "Futtassam újra? (I/N)"; cin >> ch; getline(cin, tmp); while( ch!= 'n' && ch!= 'N'); return 0; A main függvényt a többi függvény definíciója követi. 252

253 Beolvasás Egy egynél nagyobb egész szám beolvasásához az előző fejezetben bemutatott ReadInt() továbbfejlesztését, általánosítását használjuk, ahol az ellenőrzést egy külön függvény végzi el. Erre az ellenőrző függvényre a ReadInt() törzsében egy speciális változóval (check) hivatkozunk. Ez a változó eddig még nem látott módon úgynevezett függvény típusú, azaz egy függvény adható neki értékül. Nekünk most olyan ellenőrzőfüggvényre van szükségünk, amely egy egész számot vár bemenő paraméterként és egy logikai értéket ad vissza, tehát a paraméterváltozónk a bool check(int) segítségével deklarálható. Amikor a ReadInt()törzsében a check( ) kifejezés (az argumentuma helyén egy tetszőleges egész szám értékű kifejezés, legegyszerűbb esetben egy egész típusú változó állhat) kiértékeléséhez ér a vezérlés, akkor ez meghívja a check változóban tárolt függvényt az argumentumában levő kifejezés értékével, majd a függvény által visszaadott értéket veszi fel. Mivel a feladatban egynél nagyobb egész számra van szükségünk, ezért a check változónak olyan ellenőrző függvényt kell értékül adnunk, amelyik egy egész számra akkor ad igaz választ, ha az nagyobb, mint 1. Ilyen függvény az alábbi: bool GreaterThanOne(int n){ return n>1; Jól látható, hogy ennek a függvénynek a típusa megegyezik a ReadInt() harmadik paraméterváltozójának, a check változónak a típusával. A check paraméterváltozónak alapértelmezés szerinti értéket is lehet adni. Ez arra szolgál, hogy ha a ReadInt() hívása esetén nem adnánk meg a harmadik paramétert, akkor az alapértelmezés szerinti függvény legyen a check -ben tárolt ellenőrző függvény. Kézenfekvő választás erre a minden egész számot elfogadó All() függvény. Ebben az alkalmazásunkban ugyan nem hívódik meg ez a függvény, de jelenléte lehetőséget ad a ReadInt() későbbi általános használatához. 253

254 bool All(int n) { return true; A ReadInt()mindhárom paraméterváltozója egy-egy hivatkozás. Az első két esetben ezt a & jel mutatja, a harmadik pedig hivatalból az, mert ez típusa alapján egy ellenőrző függvényre hivatkozik. Ugyanakkor mindhárom hivatkozás szerinti paraméterváltozó kizárólag bemenő adatforgalmat bonyolít le. Az első két esetben ezt a const kulcsszó jelzi, a harmadik esetben csak az önmegtartóztatás, amely nem engedi meg a check változó megváltoztatását. int ReadInt(const string &msg, const string &errmsg, bool check(int)= All) { int n; int error = true; string tmp; do{ cout << msg; cin >> n; if(error = cin.fail()!check(n)){ cout << errmsg << endl; cin.clear(); getline(cin,tmp); while(error); 254

255 return n; A ReadInt() működése akkor fejeződik be, ha sikerül megadnia a felhasználónak egy megfelelő egész számot (mi esetünkben egynél nagyobbat), és ilyenkor ezzel az egész számmal tér vissza. 255

256 Absztrakt program kódolása A Divisor() függvény, attól függően, hogy melyik absztrakt programot valósítjuk meg, vagy a kiválasztásra épülő változat kódját, vagy a lineáris keresésre épülő változatét tartalmazza. Mindkét esetben egyetlen bemenő paramétere lesz: a korábban beolvasott egynél nagyobb egész szám, a visszatérési értéke pedig a megadott számnak önmagától különböző legnagyobb osztója. A kiválasztás: int Divisor(int n) { int d; for(d = n/2; n%d!= 0; --d); return d; A keresés (ehhez szükség van a cmath csomagra): int Divisor(int n) { int k; bool l = false; for(int i = 2;!l && i<=sqrt(n); ++i){ l = n%i == 0; k = i; 256

257 if(l) return n/k; else return 1; 257

258 Tesztelés Modulonkénti tesztelést végzünk. A Divisor() függvény fekete doboz teszteléséhez (a lineáris keresést vizsgáljuk, de ennek tesztesetei a kiválasztáshoz is jók) a feladat érvényes tesztesetei használhatóak. Külön fehér doboz tesztelésre nem lesz szükség: 1. Intervallum teszt a. üres intervallum: 2 esetén az osztó 1. b. egyelemű intervallum: 3 esetén az osztó 1. c. intervallum eleje: 13 esetén az osztó 1. d. intervallum vége: 16 esetén az osztó Lineáris keresés tesztje (mindig lesz keresett elem) a. Egyetlen keresett elem: 13 esetén az osztó 1. b. Több keresett elem: 16 esetén az osztó Különleges értékek tesztje a. Prím számok esetén: 2, 3, 13 esetén az osztó 1. b. Páros számok esete: 34 esetén az osztó 17. c. Általános (páratlan, nem prím): 135 esetén az osztó 45. d. Szám fele/négyzetgyöke nem egész: 33 esetén az osztó 11. A ReadInt()tesztelésénél egyrészt ki kell próbálni, hogy az All() ellenőrző függvényre jól működik-e, majd azután azt is, hogy a GreaterThanOne() mellett is megfelelő a működése. Fehér doboz tesztelésnél egymás után többször is érvénytelen adatot kell megadni. 1. Egész számok (-302, -1, 0, 1, 2, 15) beolvasása. 2. Negatív egész számok esete. 3. Nem egész szám esete. A main() fekete doboz tesztelésénél a feladat érvénytelen tesztadatait kell kipróbálni, de ez itt már nem vezet újabb tesztesetekhez. Fehér doboz tesztesetekkel a do-while ciklust kell lefedni (a ciklusmag egyszer illetve többször fusson le). 258

259 Teljes program Tekintsük meg a teljes programot. #include <iostream> #include <string> #include <cmath> using namespace std; bool GreaterThanOne(int n); bool All(int n); int ReadInt(const string &msg, const string &errmsg, bool check(int) = All); int Divisor(int n); int main() { cout << "Legnagyobb osztó!\n"; char ch; string tmp; do{ int n = ReadInt("A szám: ", "1-nél nagyobb szám kell!", GreaterThanOne); 259

260 cout << "Osztó: " << Divisor(n) << endl; cout << "Futtassam újra? (I/N)"; cin >> ch; getline(cin, tmp); while( ch!= 'n' && ch!= 'N'); return 0; int Divisor(int n) { int d; for(d = n/2; n%d!= 0; --d); return d; bool All(int n) { return true; bool GreaterThanOne(int n){ return n>1; int ReadInt(const string &msg, const string &errmsg, { bool check(int)) 260

261 int n; int error = true; string tmp; do{ cout << msg; cin >> n; if(error = cin.fail()!check(n)){ cout << errmsg << endl; cin.clear(); getline(cin,tmp); while(error); return n; 261

262 17. Feladat: Legkisebb adott tulajdonságú elem Keressük meg egy szöveges állományból feltöltött egész értékű tömbben a legkisebb olyan számot, amely k-val osztva 1-et ad maradékul! Az állomány csak egész számokat tartalmaz. A legelső szám a beolvasandó számok darabszáma, amelyet megfelelő számú (legalább darabszámnyi) egész szám követ elválasztó jelekkel (szóköz, tabulátor-, sorvége jel) határolva. Specifikáció A feladat elvi megfogalmazásában egy tömb a bemenő adat, továbbá három féle eredmény van: logikai érték, amely jelöli, hogy van-e egyáltalán a tömbnek k-val osztva 1-et adó eleme, ha igen melyik a legkisebb, és ez hányadik indexű helyen található. A = ( t : Z n, k : Z, l :, min, ind : Z ) Ef = ( t=t k=k ) n Uf = ( t=t l, min, ind min t[ i] i 1 t[ i]mod k 1 Absztrakt program A feladatot a feltételes maximumkeresés programozási tételére vezethetjük vissza. l := hamis i: Z i := 1.. n t[i] mod k 1 t[i] mod k=1 l t[i] mod k=1 l t[i]<min l,min,ind := igaz, t[i], i SKIP min,ind := t[i],i SKIP 262

263 Implementálás A tömböt vector<int> típussal definiáljuk, amelynek elemei 0-tól indexelődnek, ezért az absztrakt algoritmus ciklusa 0..n 1 intervallumot futja majd be. A program most is három részre tagolódik: beolvasás, feldolgozás, kiírás, amelyek közül az első kettőt önálló alprogramként valósítjuk meg. Szükségünk lesz egy egész számot beolvasó függvényre és két ellenőrző függvényre is: az egyik minden egész számot elfogad, a másik csak a nullától különbözőeket. Ennél fogva a forrásállományban (legyen ennek a neve main.cpp) a main függvényen kívül még öt másik függvény is megjelenik, amelyek közül négyet a main függvény törzsében használunk. int main() { vector<int> t; Fill(t); int k = ReadInt("A szám, amivel osztunk: ", "Nemnulla egész szám kell!", NotNull); int min, ind; if(condminsearch(t,k,min,ind)) cout << "A legkisebb keresett szám: " << min << "ami a(z) " << ind+1 << "-edik.\n"; else cout <<"Nincs keresett szám!\n"; return 0; 263

264 Függvények hívási láncolata A main függvény kódjából jól látható a program függvényeinek hívási szerkezete. Ezen hívási láncok mentén történik az adatáramlás az egyes függvények között. A main() egy tömböt kap a Fill() függvénytől, egy nullától különböző egész számot a ReadInt()-től. A ReadInt() a beolvasott számot a NotNull() függvénynek adja oda vizsgálatra, amelyik egy logikai értékkel jelzi vissza annak helyességét. A CondMinSearch() megkapja a tömböt, a nullától különböző számot, és visszaadja a minimális adott számmal osztható legkisebb tömbelem értékét és indexét, valamint egy logikai értéket, hogy volt-e egyáltalán adott számmal osztható elem a tömbben. Fill() main() ReadInt() NotNull() CondMinSearch() Beolvasás 7-3. ábra. Alprogramok hívási láncai Mielőtt a tömböt feldolgozzuk, először fel kell tölteni azt egy szöveges állományból. A Fill() eljárás egy vector<int> típusú objektumot ad vissza. Hasonlót láthattunk a 14. feladat megoldáskor a FillVector() alprogramban, de ott függvényértékként adtuk vissza a tömböt, most pedig eredmény paraméterváltozóval. Ennek hosszát azután állíthatjuk be, hogy beolvastuk az állományból az elemek darabszámát, ami kötelezően az állomány első adata. Ezt követően töltjük fel a tömböt az állományban található egész számokkal. Adatellenőrzést nem végzünk, mert feltesszük, 264

265 hogy az állomány megfelelően van kitöltve, csak azt vizsgáljuk, hogy az állomány nevét jól adták-e meg. Az osztó (k) beolvasását az előző fejezetben bevezetett ReadInt() függvénnyel végezzük, amelynek most azt kell vizsgálnia, hogy a megadott szám nullától különböző-e. Ehhez el kell készítenünk az alábbi ellenőrző függvényt, amelyet a ReadInt() hívásakor annak harmadik paramétereként kell megadnunk. bool NotNull(int n){ return n!= 1; Természetesen a kód tartalmazza a ReadInt() függvény definícióját és az annak deklarációjában szereplő alapértelmezett paraméterértékként szereplő All() függvény definícióját is. Absztrakt program kódolása Feltételes minimumkeresés kódja a fejezet bevezetőjében elmondottak alapján készült. bool CondMinSearch(const vector<int> &t, int k, int& min, int& ind) { bool l = false; for(int i=0; i<(int)t.size(); ++i){ if (t[i]%k!= 1) continue; if(l){ if(t[i]<min){ min = t[i]; ind = i; else {l = true; min = t[i]; ind = i; 265

266 return l; Tesztelés Most is modulonkénti tesztelést végeztünk. A CondMinSearch() teszteléséhez az érvényes tesztadatok szolgálnak tesztesetként. 1. Intervallum teszt a. (üres intervallum) Nulla darab szám esete: az állomány egy 0 értéket tartalmaz, a helyes válasz az, hogy nem találtunk keresett tulajdonságú elemet. b. (intervallum eleje) Legelöl található a legkisebb k-val osztva az 1 maradékot adó szám. c. (intervallum vége) Leghátul a található a legkisebb k-val osztva az 1 maradékot adó szám. 2. Programozási tétel tesztje a. Egyetlen, k-val osztva nem az 1 maradékot adó szám esete: a helyes válasz az, hogy nem találtunk keresett tulajdonságú elemet. b. Egyetlen, k-val osztva az 1 maradékot adó szám esete: a helyes válasz ez a szám. c. Az legelső szám k-val osztva nem az 1-et adja maradékul, de a k-val osztva 1 maradékot adó számok közül a legkisebb van legelőrébb. d. A legutolsó szám k-val osztva nem az 1-et adja maradékul, de a k-val osztva 1 maradékot adó számok közül a legkisebb van leghátrébb. e. A legkisebb, k-val osztva 1 maradékot adó szám többször is előfordul, legelöl is, leghátul is, csak középen 3. Különleges értékek tesztje a. Negatív számok is legyenek a tömbben 266

267 b. Az osztó lehet 1, páros, páratlan, prím, negatív. Az érvénytelen tesztadatok a beolvasást végző alprogramok számára biztosítanak teszteseteket. A Fill() eljárásnál azonban nem kell érvénytelen adatokra számítani, mivel a szöveges állományban megállapodás szerint helyesen van kitöltve. Fehér doboz teszteléséhez meg kell vizsgálni az alábbiakat: 1. Rossz állomány név megadása egymás után többször is. 2. Különböző hosszú tömbök esetei. A ReadInt()tesztelésénél egyrészt ki kell próbálni, hogy az All() ellenőrző függvényre jól működik-e, majd hogy a NotNull() mellett is. Fehér doboz tesztelésnél egymás után többször is érvénytelen adatot kell megadni. 1. Egész számok (-302, -1, 0, 1, 15) beolvasása. 2. Negatív egész számok esete. 3. Nem egész szám esete. A main() fekete doboz tesztelésénél a feladat érvénytelen tesztadatait kellene kipróbálni, de ez itt már nem vezet újabb tesztesetekhez. 267

268 Teljes program #include <iostream> #include <fstream> #include <vector> #include <string> using namespace std; void Fill(vector<int> &t); bool CondMinSearch(const vector<int> &t, int k, int &min, int &ind); bool NotNull(int n); bool All(int n); int ReadInt(const string &msg, const string &errmsg, bool check(int) = All); int main() { vector<int> t; Fill(t); int k = ReadInt("A szám, amivel osztunk: ", "Nemnulla egész szám kell!", NotNull); int min, ind; if(condminsearch(t,k,min,ind)) 268

269 cout << "A legkisebb keresett szám: " << min << "ami a(z) " << ind+1 << "-edik.\n"; else cout <<"Nincs keresett szám!\n"; return 0; bool CondMinSearch(const vector<int> &t, int k, int& min, int& ind) { bool l = false; for(int i=0; i<(int)t.size(); ++i){ if (t[i]<=0) continue; if(l){ if(t[i]<min){ min = t[i]; ind = i; else {l = true; min = t[i]; ind = i; return l; void Fill(vector<int> &t) { ifstream f; bool hiba; string str; do{ cout << "Fájl neve:"; 269

270 cin >> str; f.open( str.c_str() ); if ( hiba = f.fail() ){ cout << "Nincs ilyen nevű fájl" << endl; f.clear(); while (hiba); int n; f >> n; t.resize(n); for(int i=0; i<n; ++i){ f >> t[i]; f.close(); bool All(int n) { return true; bool NotNull(int n){ return n!= 1; int ReadInt(const string &msg, const string &errmsg, bool check(int)) { int n; 270

271 int error = true; string tmp; do{ cout << msg; cin >> n; if(error = cin.fail()!check(n)){ cout << errmsg << endl; cin.clear(); getline(cin,tmp); while(error); return n; 271

272 18. Feladat: Keressünk Ibolyát Egy tömb keresztneveket tartalmaz. Van-e a keresztnevek között Ibolya illetve minden név Ibolya-e? Specifikáció Ez itt két különböző feladat, ezért külön-külön oldjuk meg őket. A specifikációknak azonban csak az utófeltétele tér el egymástól. A = ( t : String n, l : ) Ef = ( t=t ) n Uf = ( t=t l search t[ i] " Ibolya " ) i 1 n Uf = ( t=t l search t[ i] " Ibolya ") i 1 Absztrakt program Az első feladatot az úgynevezett normális (pesszimista) lineáris keresésre, a másodikat az optimista lineáris keresésre vezetjük vissza. l, i := hamis, 1 i:z l, i := igaz, 1 i:z l i n l i n l := t[i] = Ibolya l := t[i] = Ibolya i := i + 1 i := i + 1 Implementálás Mindkét program három részre tagolódik: beolvasás, feldolgozás, kiírás. A beolvasás egyformán néz ki mindkét esetben, ezt a Fill() alprogram valósítja meg. A tömböt vector<string> típussal definiáljuk, amelynek 272

273 elemei 0-tól indexelődnek, ezért az absztrakt algoritmus ciklusa 0..n 1 intervallumot futja majd be. Eltér viszont a két programban a feldolgozás. Egyikben a LinSearch(), a másikban az OptLinSearch() kap szerepet, és természetesen különbözik az eredmény kiírását kísérő szöveg. Az alábbiakban mindkét változatot megmutatjuk. Az első változat main függvénye az alábbi: int main() { vector<string> t; Fill(t); if(linsearch(t)) cout << "Van Ibolya.\n"; else cout << "Nincs Ibolya.\n"; return 0; A második változat main függvénye pedig az alábbi: int main() { vector<string> t; Fill(t); if(optlinsearch(t)) cout << "Minden név Ibolya.\n"; van.\n"; else return 0; cout << "Ibolyától eltérő név is 273

274 Függvények hívási láncolata A main egy tömböt kap a Fill() függvénytől. A LinSearch() vagy az OptLinSearch() megkapja a tömböt, és visszaadja a vizsgálat eredményét mutató logikai értéket. main() Fill() LinSearch() / OptLinSearch() 7-4. ábra. Alprogramok hívási láncai Beolvasás A Fill() eljárás a 15. feladatnál látott FillVector() eljárással majdnem azonos. Egyetlen különbség ez, hogy most nem egész számokat tartalmazó tömböt, hanem sztringeket tartalmazó tömböt kell feltöltenünk. A szöveges állományban elválasztó jelekkel határolva kell megadni a neveket (egy néven belül nincs elválasztó jel). void Fill(vector<string> &t) { ifstream f; bool hiba; string str; do{ cout << "Fájl neve:"; cin >> str; f.open( str.c_str() ); 274

275 if ( hiba = f.fail() ){ cout << "Nincs ilyen nevű fájl" << endl; f.clear(); while (hiba); f >> str; while(!f.eof()){ t.push_back(str); f >> str; f.close(); Absztrakt program kódolása Nem szorul különösebb magyarázatra a két változat absztrakt programjának kódolása. Az első változat: bool LinSearch(const vector<string> &t) { bool l = false; for(int i = 0;!l && i<(int)t.size(); ++i){ l = "Ibolya" == t[i]; 275

276 return l; A második változat: bool OptLinSearch(const vector<string> &t) { bool l = true; for(int i = 0; l && i<(int)t.size(); ++i){ l = "Ibolya" == t[i]; return l; Mindkettőt a lineáris keresés kódolásánál bevezetett minta alapján készítjük el. Mindkettő a tömböt kapja meg bemenő adatként és egy logikai értéket ad vissza visszatérési értékként. 276

277 Tesztelés A specifikáció alapján felírt érvényes tesztesetek (érvénytelen teszteset most nincs): 1. Intervallum tesztje: a. (üres intervallum) Nincs egyetlen név sem. Első változatnál Nincs Ibolya, második változatnál: Mindenki Ibolya. b. (intervallum eleje) Csak az első Ibolya. Első változatnál Van Ibolya, második változatnál: Van nem Ibolya. c. (intervallum vége) Csak az utolsó Ibolya. Első változatnál Van Ibolya, második változatnál: Van nem Ibolya. 2. Programozási tétel tesztje: a. Egy Ibolya név. Első változatnál Van Ibolya, második változatnál: Mindenki Ibolya. b. Egy nem Ibolya név. Első változatnál Nincs Ibolya, második változatnál: Van nem Ibolya. c. Csupa Ibolya név. Első változatnál Van Ibolya, második változatnál: Mindenki Ibolya. d. Csupa nem Ibolya név. Első változatnál Nincs Ibolya, második változatnál: Van nem Ibolya. e. Több név, Ibolya is. Első változatnál Van Ibolya, második változatnál: Van nem Ibolya. 3. Különleges értékek tesztje: speciális sztringek a nevek helyén További teszteseteket csak a Fill() függvény tesztelése ad. 1. Rossz állomány név megadása egymás után többször is. 2. Különböző hosszú tömbök esetei. 277

278 Teljes program Tekintsük meg egyben mind a két programot. A Fill() függvényt azonban nem írjuk le részletesen, csak jelezzük a helyét. #include <iostream> #include <fstream> #include <vector> #include <string> using namespace std; void Fill(vector<string> &t); bool LinSearch(const vector<string> &t); int main() { // Adatok beolvasása vector<string> t; Fill(t); // Kiértékelés if(linsearch(t)) cout << "Van Ibolya a tömbben.\n"; else tömbben.\n"; cout << "Nincs Ibolya a 278

279 return 0; bool LinSearch(const vector<string> &t) { bool l = false; for(int i = 0;!l && i<(int)t.size(); ++i){ l = "Ibolya" == t[i]; return l; void Fill(vector<string> &t) {

280 A második program: #include <iostream> #include <fstream> #include <vector> #include <string> using namespace std; void Fill(vector<string> &t); bool OptLinSearch(const vector<string> &t); int main() { // Adatok beolvasása vector<string> t; Fill(t); // Kiértékelés if(optlinsearch(t)) cout << "Minden név Ibolya.\n"; else cout << "Van Ibolyától eltérő név is.\n"; return 0; bool OptLinSearch(const vector<string> &t) 280

281 { bool l = true; for(int i = 0; l && i<(int)t.size(); ++i){ l = "Ibolya" == t[i]; return l; void Fill(vector<string> &t) {

282 C++ kislexikon összegzés számlálás maximum kiválasztás int s = 0; for(int i=m; i<=n; ++i) s = s + f(i); int c = 0; for(int i=m; i<=n; ++i){ if( felt(i)) ++c; Value max = f(m); int ind = m; for(int i=m+1; i<=n; ++i){ if( f(i)>max){ max = f(i); ind = i; kiválasztás lineáris keresés int i; for(i = 0;!felt(i); ++i); bool l = false; int ind; for(int i=m;!l && i<=n; ++i){ l = felt(i); ind = i; optimista lineáris keresés bool l = true; int ind; for(int i=m; l && i<=n; ++i){ l = felt(i); 282

283 feltételes maximumkeresés Value max; int ind; bool l = false; for(int i=m; i<=n; ++i){ if (!felt(i)) continue; if(l){ if(t[i]>max){ max = t[i]; ind = i; else { l = true; max = t[i]; ind = i; 283

284 struktúra struct rekord{ bool l; int i, ind; bool u; ; record r; r.l = true; int j = r.ind; függvény-típusú paraméter void Name(bool fv(int, const string&)) 284

285 8. Többszörös visszavezetés alprogramokkal A programtervezés során általában is, de ebben a könyvben különösen erősen építünk a nevezetes algoritmus mintákra, a programozási tételekre, amelyekből visszavezetéssel származtatjuk a programjainkat. Már az előző fejezetben is ilyen programok implementálásával foglalkoztunk, de ott egy feladat megoldásához egyetlen programozási tételre volt csak szükség. Most viszont azt vizsgáljuk meg, hogyan érdemes összetett, több egymásba ágyazott programozási tételre támaszkodó absztrakt programot kódolni. Egy absztrakt programtervben sokszor találkozhatunk olyan (nemmegengedett) értékadással, amelynek hatása csak egy összetett (többnyire valamelyik programozási tételből származó) részprogrammal írható le. Az absztrakt program ilyen értékadása többféleképpen is értelmezhető. Tekinthetjük egyszerűen a részprogram helyét jelölő szimbólumnak, esetleg egy makró utasításnak, amely helyére bizonyos átalakítások után másolódik be a részprogram vagy a részprogramot alprogramként meghívó utasításnak. Az implementáció során kell eldönteni, hogy eme lehetőségek közül melyiket valósítsuk meg. Ha az alprogram bevezetése mellett döntünk, akkor választ kell adnunk arra is, hogy függvényként vagy eljárásként adjuk-e azt meg, majd ennek tudatában ki kell alakítani, hogyan történjen az alprogram és a környezete közötti adatáramlás. Különös gondot kell fordítani arra is, hogy a tervben található változók deklarálásának pontos helyét hol jelöljük ki a kódban. Egy absztrakt program által bevezetett változók a létrehozásuk után korlátozás nélkül használhatók, azaz globális változóként viselkednek. A kódban viszont nem lenne szerencsés a változóinkat globálisként deklarálni, ezt ahogy erre már korábban utaltunk kerülni kell. Ki kell alakítani egy olyan implementációs stratégiát, amely segítségével a tervben használt változók megfelelő módon jelennek majd meg a kódban. A programok megvalósításának kritikus része a tervezés során szándékosan nem vizsgált eseteknek a kezelése. Könnyű ugyanis azt mondani egy feladat specifikálásakor, hogy egy változó értéke nem lehet nulla, mert osztani szeretnénk vele, de a program futtatható változatában ettől még 285

286 előfordulhat a nullával való osztás, sőt az is, hogy ilyenkor nem számot ad meg a felhasználó. A program ekkor sem szállhat el, a hibát észre kell venni és annak okáról a felhasználót megfelelően kell tájékoztatni. Az ilyen vizsgálatok következtében elágazások, ciklusok tucatjai épülhetnek be a kódba, amelynek szerkezete ettől egyre összetettebb lesz. Sokkal átláthatóbbá válik a kód azonban akkor, ha minden, a tervezésnél kizárt (nem várt, hibás) esemény bekövetkezése esetén a működést külön mederbe tereljük, amelyet olyan kódrésszel írunk le, amelyet a program többi részétől elkülönítünk. Ezt a kódolási stílust hívjuk kivételkezelésnek. Implementációs stratégia Egy változó deklarációjának helyét úgy választjuk meg, hogy a láthatósága ne terjedjen túl azon a kódrészen, ahol valóban használni akarjuk. Például egy for ciklus úgynevezett indexváltozóját ha tehetjük a for ciklusban lokálisan deklaráljuk. Így járjunk el akkor is, ha egymás után több, ugyanolyan nevű, de egymástól nyilvánvalóan nem függő indexváltozót használó for ciklust készítünk. Ilyenkor minden egyes ciklusra külön-külön deklarálunk egy indexváltozót, ezáltal a ciklusok egymástól való függése csökken. Ha a programterv tartalmaz olyan alprogramokat, amelyek lokális változókat vezetnek be, akkor ezek a változók a megvalósított kódban is legyenek lokálisak az alprogramra nézve. Ugyanez igaz a tervben paraméterváltozóként feltüntetett lokális változókra is; azok legyenek paraméterváltozók a kódban is. Ettől legfeljebb csak akkor térjünk el, hogy például az alprogramot a tervvel szemben nem eljárásként, hanem függvényként akarjuk használni, és ezért néhány eredmény paraméterváltozó helyett a visszatérési érték fogja az eredményt átadni. Ekkor az eredmény paraméterváltozó közönséges lokális változó lesz, amelynek az értékét majd egy return utasítás keretében kell visszaadni. Általános szabály, hogy ha egy alprogramot függvényszerűen hívunk meg, akkor lehetőleg ne legyenek eredmény-paraméterváltozói, azaz ilyenkor a paraméterek csak a bemenő értékeket közvetítsék. Ettől a szabálytól természetesen indokolt esetben el lehet térni (amennyiben a programozási nyelv lehetővé teszi). Ilyen indok lehet, a programozási tételek kódolásánál kialakult hagyomány. Például egy lineáris keresést, amelynek 286

287 több eredménye is van (talált-e keresett elemet illetve mi a keresett elem) érdemes úgy kódolni, hogy a találat sikerét jelző logikai értéket visszatérési értékként, a megtalált elemet paraméterként adjuk vissza. (Elterjedt az megoldás is, amelyik csak a keresett elemet adja vissza, sikertelen keresés esetén egy speciális, úgynevezett extremális értékkel.) Előfordulhat olyan eset, hogy egy alprogram többször is meghívódik, és a működése egy változójának az alprogram előző hívása során előállított értékétől is függ. A tervben ez a változó nyilván globális az alprogramra nézve, és még az alprogram hívásai előtt létre lett hozva. A kódban sem szabad a változót az alprogram lokális változójaként felvenni, de globális változóként való létrehozásával kapcsolatban erős ellenérvek vannak. Lokális változó: Az adott utasításblokkban definiált változó, amely a definíció kezdetétől a blokk végéig látható, feltéve, hogy egy beágyazott blokkban nem definiáljuk felül. A beágyazott blokk egy lokális változójának láthatósága ugyanis elfedi a külső blokk azonos nevű változójának láthatóságát. Globális változó: Egy beágyazott utasításblokkban használt olyan változó, amelyiknek a definíciója a külső blokkban van, és ebből fakadóan a láthatósága is túlterjed a vizsgált beágyazott blokkon ábra. Program változóinak láthatósági kategóriái Mindenekelőtt tisztában kell lennünk azzal, hogy lényeges különbség van a tervezésnél bevezetett globális változó és a programozási nyelvekben használt globális változó fogalmai között. Többször említettük már, hogy a programtervben bevezetett változók globálisak a programra nézve: létrehozásuk és megszüntetésük között a program szabadon használhatja őket. Ennek megfelelően minden változó, amely egy alprogram hívásakor még él (azaz korábban jött létre, és még nem szűnt meg) az globális az alprogramra nézve, tehát az alprogramban is használható. A programozási nyelvekben a globális változó jóval árnyaltabb fogalom a tervezésnél látott 287

288 értelmezésnél. Egyfelől beszélhetünk abszolút globális változóról, amely valóban a teljes programban látható (ilyet nem is olyan könnyű deklarálni, csak bizonyos helyeken és formában deklarált változók lehetnek a teljes programra nézve globálisak), de beszélhetünk egy alprogram szempontjából vett, azaz relatív globális változóról, amelyet nem az alprogramban deklarálunk, de ott látható és használható. Az abszolút globális változók használata erősen ellenjavallott. Nagyon indokolt esetben, kevés számú változó számára megengedhetjük, hogy ha egy változót több alprogram is használ, akkor az legyen ezekre nézve globális. Egy nagyméretű program esetében viszont az szinte kizárt, hogy olyan változónk legyenek, amelyet a program minden részében használni kell, azaz amelyet indokolt általánosan globálisnak választani. Szerencsére egy kiterjedt, több forrásállományra tördelt program esetében nem is olyan egyszerű abszolút globális változót deklarálni a programozási nyelvekben. A csak néhány alprogramra nézve globális változók bevezetése esetén ha mégis ehhez folyamodnánk pontosan kell dokumentálni, hogy melyik alprogram olvassa, melyik írhatja felül ennek a változónak az értékét. Az ilyen relatív globális változók deklarálásának módja erősen eltér a különböző programozási nyelvekben. Egy igazi blokkstrukturált nyelvben (ilyen például a Pascal nyelv) az alprogramokat egymásba ágyazhatjuk. Ilyenkor a belső alprogram használhatja az őt tartalmazó alprogramok lokális változóit, feltéve, hogy ugyanolyan névvel nem vezetünk be új változókat. A beágyazó alprogram lokális változója tehát globális változóként jelenik meg a beágyazott alprogramban. A C++ nyelvben viszont az alprogramok nem ágyazhatóak egymásba, egymás után definiálhatjuk csak őket. Ezek az alprogramok hívhatják ugyan egymást, de nem látják, nem használhatják egymás lokális változóit. Lehetőség van ugyanakkor az alprogramokon kívül változókat definiálni, de ilyenkor ezek az összes alprogramra nézve lesznek globálisak, hacsak nem vetünk be egyéb, a láthatóságot korlátozó nyelvi elemet (névtér, önálló fordítási egység, osztály). Sose felejtsük el, hogy a globális változók használata rontja a program átláthatóságát, növeli a rejtett hibák bekövetkezésének lehetőségét, mert 288

289 sérti a lokalitás elvét, nevezetes azt, hogy egy adott programrészről önmagában, a környezetének ismerete nélkül lehessen látni, milyen adatokból milyen eredmény állít elő. Mit tegyünk akkor, ha a programterv globális változót használ egy alprogramban, de a megvalósításban ezt ki szeretnénk küszöbölni? Ennek legegyszerűbb módja az, ha a kérdéses változót az alprogramot hívó környezet lokális változójává tesszük, és paraméterként adjuk át az alprogram egy erre a célra bevezetett új paraméterváltozójának. Ez az adatcsere irányától függően lehet bemenő- vagy eredmény paraméterváltozó, esetleg mindkettő egyszerre. Ha az alprogram csak megváltoztatja a kiküszöbölendő globális változó értékét, de nem használja fel bemeneti adatként, akkor az alprogramot olyan függvényként is meglehet valósítani, amely a kérdéses változó számára állít elő új értéket, azaz a hívó környezet szóban forgó változója a függvényhívást tartalmazó értékadás baloldalán jelenik meg. Kivételnek a nem várt, vagy a megoldandó feladat szempontjából marginális eseményt nevezzük. Ilyen lehet például egy olyan hiba, amelyet egy érvénytelen teszteset, azaz a specifikáció előfeltételében kizárt adat okoz. A programterv nem számol az ilyen esetekkel, de az implementációnál az a cél, hogy ne következhessen be a program futása során olyan esemény, amelyre nincs program által leírt válasz. A programozónak azt kell eldöntenie, hogy egy kivétel bekövetkezését eleve elkerülje-e, vagy a bekövetkezés esetén hajtson végre egy speciális tevékenységet a programja. Ha például el akarjuk kerülni egy bemenő adattal történő nullával való osztást, akkor célszerű nem megvárni az osztáskor bekövetkező hibát, és azt kezelni, hanem jobb megelőzni az osztó beolvasásakor végzett ellenőrzéssel (elágazással vagy hátul-tesztelő ciklussal). Azt a hibát viszont, ha egy nem létező állományt akarunk megnyitni, sokszor éppen azáltal fedjük fel, hogy megkíséreljük a nyitást. A fájlnyitási hiba bekövetkezésének lekezelésére ilyenkor megfelelő kódot lehet készíteni. Ez történhet helyben (elágazással vagy hátul-tesztelő ciklussal) vagy amennyiben a nyelv támogatja kivételkezeléssel, amikor a program végrehajtását mintegy annak strukturált szerkezetéből kilépve, speciális helyen leírt kódra bízzuk. Ennek befejeződésekor a vezérlés meg is 289

290 állhat, de vissza is terelhető a normális működést leíró kódra. Habár nyelvi szempontból csak ez utolsó, speciális nyelvi elemet igénylő megoldást szokás kivételkezelésnek nevezni, fogalmilag minden olyan implementációs megoldás idesorolható, amely a kivételes működés leírására szolgál. Nyelvi elemek A több programrészből álló alkalmazások implementálásánál fontos pontosan ismerni, hogy a választott programozási nyelvben milyen láthatósági szabályok vannak, hogyan lehet globális és lokális változókat használni. Ehhez meg kell ismernünk, hogy milyen szerkezetű programok készíthetők az adott nyelven és ez a szerkezet milyen hatással van a változók globális és lokális láthatóságának megválasztására. Sok nyelvben utasításblokkokat lehet kijelölni a láthatóság korlátozására, és egy ilyen blokk szempontjából lehet beszélni egy változó lokális vagy globális láthatóságáról. A Pascal nyelvben csak az alprogramok képeznek önálló utasításblokkot, de ezeket egymásba lehet ágyazni. A C++ nyelven az alprogram blokkok nem ágyazhatók egymásba, ellenben egyéb láthatóságot korlátozó utasításblokkok (for ciklus indexváltozója, kapcsos zárójelek közé zárt részek) is létrehozhatók. A C++ nyelvbeli globális változók deklarálásának módozatait nem mutatjuk be, mert a paraméterátadásra épülő adatcserének alkalmazását részesítjük előnyben. Ennek nyelvi eszközeit az előző két fejezetben viszont már megismertük. Ha a kivételkezelésen olyan programrészek kódolását értjük, amelyek végrehajtásához a program strukturált szerkezetéből történő kilépésre van szükség, akkor az a legfontosabb kérdés, hogy a választott programozási nyelv milyen nyelvi elemekkel támogatja ennek megvalósítását. Akár már egy fegyelmezetten használt goto utasítás is lehet a kivételkezelés eszköze, de jobban örülünk, ha erre külön nyelvi elemek állnak rendelkezésünkre. A kivételkezelésnek két lényeges pontja van. Az egyik a kivétel keletkezésének, a kivétel eldobásának a helye, amikor a program normális vezérlése megszakad, a vezérlés kiugrik a kódból. A másik a kivétel kezelése, a kivétel elkapásának helye, ahol a vezérlés egy speciális 290

291 tevékenység végrehajtásával folytatódik, majd visszatér a program normális vezérléséhez. Ha egy kivételt nem kapunk el, akkor a program abortál. Egy program működésében lehetnek előre definiált kivételek (nullával való osztás, index túlcsordulás, stb.) de a programozó által definiált úgynevezett felhasználói kivételek is. Az előbbi esetben a kivétel dobása automatikusan történik, az utóbbi esetben explicit módon kell azt kikényszeríteni (throw). (Általában lehetőség van előre definiált kivételek felhasználó által kényszerített kiváltására is.) A kivételek speciális értékek, objektumok, amelyeknek van típusa, értéke, és ennél fogva képesek a kiváltó okra vonatkozó információt eltárolni. Kivételt csak a kód azon részében tudjuk elkapni és kezelni, amelyik kódrészt speciális módon megjelöltünk (megfigyelt szakasz, try-blokk). Miután azt a kód szakaszt, ahol kivétel keletkezésre számítunk, megjelöltük, ehhez a szakaszhoz úgynevezett kivételkezelő ágakat (kezelő ág, catch-blokk) rendelhetünk, amelyek mindegyike egy bizonyos kivételtípussal foglalkozik: meghatározott típusú kivétel bekövetkezése esetén az ahhoz tartozó kivételkezelő ág fog végrehajtódni. Ha a kivételkezelés mást nem mond (nem terminálja a programot, nem dob egy másik kivételt), akkor a vezérlés a megfigyelt kódszakasz utáni utasításon folytatódik tovább. Bizonyos nyelvekben olyan kódrészt (lezáró szakasz, finally-blokk) is hozzárendelhetünk egy megfigyelt szakaszhoz, amelynek végrehajtására mindenképpen sor kerül, akkor is, ha nem következett be kivétel a megfigyelt szakaszon, és akkor is, ha kivételkezelésre került sor. 291

292 19. Feladat: Kitűnő tanuló Egy iskola egyik n diákot számláló osztályában m különböző tantárgyból osztályoztak a félév végén. A jegyek egy táblázat formájában rendelkezésünkre állnak. (A diákokat és a tantárgyakat most sorszámukkal azonosítjuk.) Állapítsuk meg, van-e olyan diák, akinek csupa ötöse van! Specifikáció Ennek a feladatnak a megoldását az első kötetben már megterveztük. A = ( napló:n n m, van: ) Ef = ( napló = napló ) Uf = ( napló = napló van = i [1..n]: színjeles(i) ) Absztrakt program ahol színjeles : [1..n] színjeles(i) = j [1..m]: napló[i,j]=5 Az absztrakt program két egymásba ágyazott lineáris keresés. van,i := hamis,1 i:n van i n van := színjeles(i) i := i+1 van:=színjeles(i) van,j := igaz,1 j:n van j m van := napló[i,j]=5 j := j+1 292

293 Az absztrakt program második részét (alsó szint) tekinthetjük egy közönséges részprogramnak, de alprogramként is felfogható. Előbbi esetben a részprogramot egyszerűen behelyettesítjük a felső szint van:=színjeles(i) nem-megengedett értékadás helyébe, utóbbiban alprogram-hívásként tekintünk erre az értékadásra, amely a végrehajtása során átadja a vezérlést az alprogramnak. Ha a második változat mellett döntünk, akkor azt a mátrixot, amelyre a fenti tervben globális változóként hivatkozik az alprogram, elérhetővé kell tenni az alprogram kódjában, és arról is döntenünk kell, hogy az alprogramot függvényszerű vagy eljárásszerű hívással aktivizáljuk-e. Ezek olyan nyitott kérdések, amelyeket az implementáció során kell megválaszolnunk. Implementálás A megoldó programban külön függvényben valósítjuk meg az absztrakt program alprogramját, az osztályzási napló feltöltését, valamint ez utóbbi által hívott egy egész szám beolvasását végző függvényt, amelyet többféle ellenőrzési lehetőséggel is fel lehet ruházni. Az absztrakt algoritmus főprogramja közvetlenül a main függvénybe kerül. Főprogram int main() { // Osztályzási napló feltöltése vector<vector<int> > reg; ReadMarks(reg); // Lineáris keresés bool exists = false; for(int i=0;!exists && i<(int)reg.size(); ++i){ 293

294 exists = Excellent(reg[i]); // Eredmény kiírása if(exists) cout << "Van kitűnő diák.\n"; else cout << "Nincs kitűnő diák.\n"; return 0; A főprogram három részre tagolódik. Deklarálja a feladat bemenő (reg) és eredmény (exists) változóját, meghívja a mátrixot feltöltő ReadMarks() eljárást, amelyet az absztrakt algoritmus főprogramja követ, végül az eredmény kiírására kerül sor. Absztrakt alprogram kódolása A kitűnő diákot vizsgáló alprogramot függvényszerű hívással aktiváljuk, az eredményt visszatérési értékként adjuk meg. Az alprogramnak nemcsak az aktuális diák sorszáma (i) a bemenő adata (mint ahogy ezt a programterv felületes vizsgálata sugallja), hanem szükség van az osztályzatokat tartalmazó naplóra (reg) is. Valójában a mátrixnak csak az a sora kell, ahol az i-edik diák jegyei találhatóak. Ezért az alprogram bemenő paramétere a mátrix aktuális sora lesz, ami egy egydimenziós tömb, és magát a diák sorszámát nem is kell átadni. bool Excellent(const vector<int> &v) { bool l = true; 294

295 for(int j=0; l && j<(int)v.size(); ++j){ l = 5 == v[j]; return l; Függvények hívási láncolata Az alábbi kapcsolatrendszerből az olvasható ki, hogy a programban szerepelő függvények melyik másik függvényeket hívják meg a működésük során. main() ReadMarks() Excellent() ReadInt() Nat() Mark() 8-2. ábra. Alprogramok hívási láncai A hívási láncok mentén adatcsere zajlik az egyes függvények között. (Globális változót nem használunk.) Az osztályzási naplót a billentyűzetről olvassa be a ReadMarks() eljárás, és egy paraméterváltozó segítségével adja vissza a főprogramnak. Ennek a mátrixnak az aktuális sorát kapja meg az Excellent() függvény, amelyik egy logikai értéket ad vissza: igazat, ha a sor minden eleme ötös, hamisat, ha nem. A ReadMarks() függvény egész számokat olvas be a ReadInt() segítségével, amelyek egy része (diákok és tárgyak száma) természetes szám kell legyen, amelyet a logikai értéket visszaadó Nat() függvény vizsgál; másik része kizárólag 1-től 5-ig terjedő egész szám lehet, amit a logikai értéket visszaadó Mark() függvény ellenőriz. Bemenő adatok beolvasása void ReadMarks(vector<vector<int> > &reg) 295

296 { int n = ReadInt("Tanulók száma: ", "Természetes szám!", Nat); int m = ReadInt("Tárgyak száma: ", "Természetes szám!", Nat); reg.resize(n); for(int i=0; i<n; ++i){ reg[i].resize(m); cout << i+1 << ". tanuló eredményei\n"; for(int j=0; j<m; ++j){ cout << "\t" << j+1 << ". tantárgy: "; reg[i][j] = ReadInt("", "1 és 5 közötti szám!", Mark); A ReadMarks()egy n m-es mátrix méretét határozza meg, majd 1 és 5 közötti egész számokkal feltölti. A mátrixot eredmény paraméterváltozó segítségével adja vissza (hivatkozás szerinti paraméterátadás). A tervvel ellentétben a mátrix sorai és oszlopai 0-val kezdődően indexeltek. Először a mátrix méreteinek bekérésére kerül sor, majd (ehhez a ReadInt() függvényt hívjuk meg a Nat() ellenőrző függvénnyel), majd a mátrix méretének beállítása után az elemeit olvassuk be (ReadInt() a Mark() segítségével). 296

297 Lényegesen barátságosabb a program, ha a diákokra és a tantárgyakra a nevükkel lehet hivatkozni, nem sorszámmal. void ReadMarks(vector<vector<int> > &reg) { int n = ReadInt("Tanulók száma: ", "Természetes szám!", Nat); vector<string> student(n); cout << "Adja meg a tanulók neveit:" << endl; for(int i=0; i<n; i++) { cout << i+1 << ". tanuló neve: "; cin >> student[i]; int m = ReadInt("\nTárgyak száma: ", "Természetes szám!", Nat); vector<string> subject(m); cout << "Adja meg a tantárgyakat:" << endl; for(int j=0; j<m; j++) { cout << j+1 << ". tantárgy neve: "; cin >> subject[j]; reg.resize(n); for(int i=0; i<n; ++i){ 297

298 reg[i].resize(m); cout << student[i] << " eredményei\n "; for(int j=0; j<m; j++) { cout << "\t" << subject[j] << ":"; reg[i][j] = ReadInt("", "1 és 5 közötti szám!", Mark); A ReadInt() a korábbi fejezetekből már ismert függvény, amelynek harmadik paramétere számára két speciális függvényt is készítünk: természetes számot (Nat), valamint 1 és 5 közé eső egész számot (Mark) elfogadó függvényeket. Tesztelés bool Nat(int n) { return n>=0; bool Mark(int n){ return n>=1 && n<=5; Induljunk ki a feladat specifikációjából származtatott érvényes fekete doboz tesztesetekből. A tesztesetek kialakításánál figyelembe vesszük az alkalmazott két programozási tétel és azok intervallumának tesztelési szempontjait: 1. Intervallumok tesztje: a. A diákok és a tárgyak száma nulla. Ilyenkor nincs kitűnő diák. b. A diákok száma nulla, de a tárgyak száma nem. Ilyenkor nincs kitűnő diák. c. A tárgyak száma nulla, de van diák. Ilyenkor van kitűnő diák. 298

299 d. Egy diák van egy tárggyal, ami ötös ( azaz van kitűnő diák) illetve nem ötös (azaz nincs kitűnő diák). e. Több diák közül csak az első, illetve csak az utolsó kitűnő (azaz van kitűnő diák). f. Egy diák több tárggyal, amelyek közül csak az első, illetve csak az utolsó nem ötös (azaz nincs kitűnő diák). 2. Külső lineáris keresés tesztje: a. Több diák, több tárgy, és van egy kitűnő, aki az első, vagy az utolsó, vagy a középső a diákok között. b. Több diák, több tárgy, és mindenki kitűnő. c. Több diák, több tárgy, és nincs kitűnő. 3. Belső lineáris keresés tesztje: a. Egy diák több tárggyal, amelyek közül minden ötös (azaz van kitűnő diák). b. Egy diák több tárggyal, amelyek között nem minden ötös (azaz nincs kitűnő diák). c. Egy diák több tárggyal, amelyek között csak egy nem ötös (azaz nincs kitűnő diák). Ezek az esetek lefedik a main() és az Excellent() fehér doboz tesztelését is, sőt a ReadMarks() függvényét is. Az érvénytelen adatok kiszűrését a helyesen paraméterezett ReadInt() függvény végzi. A ReadInt()tesztelésénél egyrészt ki kell próbálni, hogy az All() ellenőrző függvényre jól működik-e, majd Nat() és Mark()mellett is. Fehér doboz tesztelésnél egymás után többször is érvénytelen adatot kell megadni. 1. Egész számok (-302, -1, 0, 1, 2, 3, 4, 5, 6) beolvasása. 2. Negatív egész számok esete. 3. Nem egész szám esete. Ennek teszteléséhez ki kell próbálni a helytelen adatok (nem szám, negatív szám, 5-nél nagyobb vagy 1-nél kisebb osztályzat) beírását. 299

300 Teljes program #include <iostream> #include <vector> #include <string> using namespace std; bool Excellent(const vector<int> &v); void ReadMarks(vector<vector<int> > &reg); bool Nat(int n) { return n>=0; bool Mark(int n){ return n>=1 && n<=5; bool All(int n) { return true; int ReadInt(const string &msg, const string &errmsg, bool check(int) = All); int main() { // Osztályzási napló feltöltése vector<vector<int> > reg; ReadMarks(reg); 300

301 // Lineáris keresés bool exists = false; for(int i=0;!exists && i<(int)reg.size(); ++i){ exists = Excellent(reg[i]); // Eredmény kiírása if(exists) cout << "Van kitűnő diák.\n"; else cout << "Nincs kitűnő diák.\n"; return 0; bool Excellent(const vector<int> &v) { bool l = true; for(int j=0; l && j<(int)v.size(); ++j){ l = 5 == v[j]; return l; void ReadMarks(vector<vector<int> > &reg) { 301

302 int n = ReadInt("Tanulók száma: ", "Természetes szám!", Nat); vector<string> student(n); cout << "Adja meg a tanulók neveit:" << endl; for(int i=0; i<n; i++) { cout << i+1 << ". tanuló neve: "; cin >> student[i]; int m = ReadInt("\nTárgyak száma: ", "Természetes szám!", Nat); vector<string> subject(m); cout << "Adja meg a tantárgyakat:" << endl; for(int j=0; j<m; j++) { cout << j+1 << ". tantárgy neve: "; cin >> subject[j]; reg.resize(n); for(int i=0; i<n; ++i){ reg[i].resize(m); cout << student[i] << " eredményei\n "; for(int j=0; j<m; j++) { cout << "\t" << subject[j] << ":"; reg[i][j] = ReadInt("", 302

303 "1 és 5 közötti szám!", Mark); int ReadInt(const string &msg, const string &errmsg, bool check(int)) { int n; bool error = true; string tmp; do{ cout << msg; cin >> n; if(error = cin.fail()!check(n)){ cout << errmsg << endl; cin.clear(); getline(cin,tmp); while(error); return n; 303

304 20. Feladat: Azonos színű oldalak Adott két n oldalú egybevágó szabályos sokszög, amelyek oldalait véletlenszerűen kiszínezték. Hogyan helyezzük egymásra a két sokszöget úgy, hogy a lehető legtöbb helyen legyenek azonos színű oldalak egymáson! Specifikáció Egy sokszög oldalainak színeit egy n elemű tömbben tároljuk. Így hallgatólagosan rögzítjük, hogy melyik az első oldal, melyik a második, és így tovább. A tömböt 0-tól n 1-ig indexeljük, értékei a színek. Az eredmény egy 0 és n 1 közé eső szám lesz, amely azt mutatja meg, hogy hányszor kell az egyik sokszöget a másikhoz képest elforgatni az óra járásával megegyező irányban ahhoz, hogy a lehető legtöbb helyen kerüljenek azonos színű oldalak egymásra. A forgatás a sokszög oldalainak újra sorszámozását jelenti. Ha például az x sokszöget i egységgel elforgatjuk, akkor az így kapott változatnak a j-edik oldala az eredeti változat (i+j) mod n- edik oldala lesz. Ennek színét kell majd a másik sokszög j-edik oldalának színével összevetni. Az egyezés(i) azt adja majd meg, hogy az y sokszög hány oldalának színe egyezik meg az i egységgel elforgatott x sokszög megfelelő oldalának színével. 0 n A = ( x, y : Szinek.. 1, k:n ) Ef = ( x = x y = y n > 2 ) Uf = ( Ef max, k n 1 max egyezés ( i) ) i 0 ahol egyezés : [0..n 1] n 1 egyezés(i) = 1 j 0 y[ j] x[( i j) modn] Absztrakt program 304

305 A megoldás egy maximum kiválasztásba ágyazott számlálás. A maximum kiválasztás a különböző i-kre kiszámolt egyezés(i) értékek között keresi a legnagyobbat, a számlálás pedig az egyezés(i) értékét állítja elő. max,k := egyezés(0),0 i = 1.. n 1 i:n e := egyezés(i) e > max max, k := e, i SKIP e:=egyezés(i) e := 0 j = 0.. n 1 j:n y[j] = x[(i+j) mod n] e := e + 1 SKIP Implementálás A megoldó programban külön alprogramként valósítjuk meg az absztrakt program fő- és alprogramját, valamint egy sokszög oldalszíneinek billentyűzetről történő beolvasását. Függvények hívási láncolata 305

306 ReadInt() G2() main() ReadPoligon() MaximalFittness() IdenticalEdges() 8-3. ábra. Alprogramok hívási láncai A main függvény a már korábbról ismert ReadInt() segítségével olvassa be a sokszögek oldalszámát, ami egy 2-nél nagyobb egész szám. Ezt ellenőrzi majd a G2() függvény. Az eljárásként kódolt ReadPoligon() egy sokszöget ad vissza a mainnek. A hívások láncolata ugyan nem mutatja, de a main függvény kétszer hívja a ReadPoligon()-t, hiszen két sokszögre van szükségünk. A MaximalFittness() bemenő paraméterként megkapja ezeket és továbbítja az IdenticalEdges()-nek. A MaximalFittness() egy ciklusban annyiszor hívja meg az IdenticalEdges()-t, ahányféleképpen el lehet forgatni az egyik sokszöget a másikhoz képest. Az IdenticalEdges() egy színegyezés darabszámot ad vissza a MaximalFittness()-nek, ez utóbbi pedig egy forgatásszámot a main-nek. E két utóbbi alprogramot függvényként implementáljuk. Főprogram int main() { // Sokszögek beolvasása int n = ReadInt("A sokszögek oldalszáma: ", "2-nál nagyobb egész szám legyen!\n", G2); vector<string> x(n), y(n); 306

307 cout << "Első sokszög oldalainak színei:\n"; ReadPoligon(x); cout << "Második sokszög oldalainak színei:\n"; ReadPoligon(y); // Eredmény kiírása cout << "A második sokszöget " << MaximalFittness(x,y) << " egységgel kell elforgatni \n" ; return 0; Absztrakt program kódolása A maximum kiválasztás (MaximalFittness()) szokásos eredményei közül csak az indexet (ind) kell visszaadni, az érték (max) közönséges lokális változó lesz. A tervben globális változóként szereplő két tömbre bemenő paraméterváltozókkal hivatkozunk. Ez a függvény nem ellenőrzi, hogy a két tömb azonos hosszú-e, ezt itt feltételezhetjük. int MaximalFittness(const vector<string> &x, const vector<string> &y) { int ind = 0; int max = IdenticalEdges(x,y,0); for(int i=0; i<(int)x.size(); ++i){ 307

308 int c = IdenticalEdges(x,y,i); if(c > max){ max = c; ind = i; return ind; A számlálás (IdenticalEdges()) is megkapja paraméterként a két tömböt valamint az forgatás mértékét, és visszatérési értékként adja majd meg az illeszkedő oldalak számát. int IdenticalEdges(const vector<string> &x, const vector<string> &y, int i) { int n = (int)x.size(); int c = 0; for(int j=0; j<n; ++j){ if(x[(i+j)%n] == y[j]) ++c; return c; Sokszög beolvasása 308

309 A ReadPoligon() függvény sztringként olvassa be egy sokszög oldalainak színét. void ReadPoligon(vector<string> &t) { Tesztelés for(int i=0; i<(int)t.size(); ++i){ cout << "Az " << i+1 << " oldal színe:"; cin >> t[i]; Kezdjük most is az érvényes fekete doboz tesztesetek összegyűjtésével. 1. Intervallum tesztje: (A 0..n 1 intervallum az n>2 feltétel miatt legalább három elemű.) a. Azonosan kiszínezett sokszögek esetén nulla forgatásnál van a legtöbb egyezés. b. Három oldalú sokszögek, egy (piros, kék, sárga) és egy (sárga, piros, kék) esetén két forgatásnál van a legtöbb egyezés. 2. Maximum keresés tesztje: a. Két színnel ugyanúgy váltakozva kiszínezett sokszögek esetén több egyformán maximális egyezés van. 3. Számlálás tesztje: a. Soha egyetlen oldal színe sem azonos. Például egy (piros, piros, piros) és egy (kék, kék, kék) sokszög. b. Minden oldal színe azonos. Például két (piros, piros, piros) sokszög. c. Legfeljebb egy oldal színe azonos. Például egy (piros, kék, sárga) és egy (kék, piros, sárga) sokszög. 309

310 Ezek elegendőek a MaximalFittness() és az IdenticalEdges() fehér doboz tesztelésére is. Önmagában az IdenticalEdges() eltérő hosszú tömbök esetén kiszámíthatatlanul működik, de ezt nem megfelelő felhasználásnak tekintjük, ezért nem módosítjuk. A ReadPoligon() tesztelését a fenti tesztesetek lefedik. Ezt könnyű látni, ha az általa megoldott részfeladathoz (olvassunk be egy sokszöget) fekete doboz teszteseteket gyártunk, vagy a kód ismeretében fehér doboz tesztelést végzünk. Az érvényes adatok beolvasása a ReadInt() helyes működésén múlik. A main()tesztelése nem igényel a fentieknél újabb teszteseteket. 310

311 Teljes program #include <iostream> #include <vector> #include <string> using namespace std; int MaximalFittness(const vector<string> &x, const vector<string> &y); int IdenticalEdges (const vector<string> &x, const vector<string> &y, int i); void ReadPoligon(vector<string> &t); bool G2(int n){ return n>2; bool All(int n) { return true; int ReadInt(const string &msg, const string &err, bool check(int) = All); int main() { //Sokszögek beolvasása int n = ReadInt("A sokszögek oldalszáma: ", "2-nál nagyobb egész szám legyen!\n", G2); vector<string> x(n), y(n); cout << "Első sokszög oldalainak színei:\n"; 311

312 ReadPoligon(x); cout << "Második sokszög oldalainak színei:\n"; ReadPoligon(y); //Eredmény kiírása cout << "A második sokszöget " << MaximalFittness(x,y) << " egységgel kell elforgatni.\n"; return 0; int MaximalFittness(const vector<string> &x, const vector<string> &y) { int ind = 0; int max = IdenticalEdges(x,y,0); for(int i=0; i<(int)x.size(); ++i){ int c = IdenticalEdges(x,y,i); if(c > max){ max = c; ind = i; return ind; 312

313 int IdenticalEdges(const vector<string> &x, const vector<string> &y, int i) { int =(int)x.size(); int c = 0; for(int j=0; j<n; ++j){ if(x[(i+j)%n] == y[j]) ++c; return c; void ReadPoligon(vector<string> &t) { for(int i=0; i<(int)t.size(); ++i){ cout << "Az " << i+1 << " oldal színe:"; cin >> t[i]; int ReadInt(const string &msg, const string &errmsg, bool check(int)) { int n; int error = true; string tmp; 313

314 do{ cout << msg; cin >> n; if(error = cin.fail()!check(n)){ cout << errmsg << endl; cin.clear(); getline(cin,tmp); while(error); return n; 314

315 21. Feladat: Mátrix párhozamos átlói Döntsük el, hogy egy adott négyzetes mátrix mindegyik főátlóval párhuzamos átlójában az elemek összege nulla-e! Specifikáció A feladat pontos megfogalmazásához arra lesz szükség, hogy a főátlóval párhuzamos átlókat ügyesen megsorszámozzuk. Vegyük észre, hogy egy főátlóval párhuzamos átlóban (ilyen maga a főátló is) az elemek oszlopindexének és sorindexének különbsége állandó. A főátlóbeli elemek (t[i,i]) esetén ez a különbség 0, a főátló feletti átlóban 1 (t[i,i+1]), a legfelső átlóban (t[1,n]) n 1. Közvetlenül a főátló alatti átlóban -1 (t[i,i 1]), a legalsó átlóban 1 n (t[n,1]). A cél tehát valamelyik olyan átlót megkeresni, amelyik sorszáma az 1 n.. n 1 intervallum egy eleme. Az összeg(k) a k-adik sorszámú átló elemeinek összegét adja majd meg. A = ( t:z n n, l: ) Ef = ( t = t ) Uf = ( t = t l = k [1 n.. n 1]: (összeg(k)=0) ) ahol összeg : [1-n..n-1] Z összeg(k) = vagy összeg(k) = Absztrakt program A megoldás egy optimista lineáris keresésbe ágyazott összegzés. l,k := igaz,1 n k:n l k n 1 l := összeg(k) = 0 k := k

316 s:=összeg(k) s := 0 i = 1.. n k i:n s := s+ Implementálás A megoldó programban külön függvények tartalmazzák az absztrakt program főprogramját és alprogramját, valamint egy eljárás a mátrixot egy szöveges állomány alapján előállító kódot. Főprogram A program három részre tagolódik: a mátrix beolvasására (ReadMatrix()), az igaz/nem igaz jellegű válasz meghatározására (LinSearch()) és az eredmény kiírására. int main() { vector<vector<int> > t; ReadMatrix(t); int ind; if(linsearch(t,ind)) cout << "Minden átló elemösszege nulla.\n"; else cout << "Van nem zéróösszegű átló is.\n"; 316

317 return 0; Megjegyezzük, hogy a main függvényen kívül mindegyik függvény használja a t mátrixot, a ReadMatrix() feltölti egy állomány alapján, a többi pedig hivatkozik rá. Itt indokolt lenne a mátrixot globális változóként bevezetni, de mégsem engedünk a csábításnak. A mátrixot a main függvényben definiáljuk, és az erre történő hivatkozást adjuk át a másik három függvénynek. Függvények hívási láncolata main() ReadMatrix() LinSearch() Summation() 8-4. ábra. Alprogramok hívási láncai Absztrakt program kódolása Az optimista lineáris keresést a LinSearch() függvény tartalmazza, amelynek bemenő paraméterváltozója hivatkozik a mátrixra, amit aztán tovább is ad az összegzést tartalmazó Summation() alprogramnak. A LinSearch()eredménye a keresés sikerét jelző logikai érték, amely visszatérési értékként jut el a hívás helyére. bool LinSearch(const vector<vector<int> > &t) { int n = (int)t.size(); bool l = true; for(int k=1-n;!l && k<=n-1; ++k){ 317

318 l = 0 == Summation(t,k); return l; A Summation() a mátrixra hivatkozó paraméterváltozó mellett bemenő paraméterként kapja meg a tervben is paraméterként szereplő átló sorszámot, és az átló összegét visszatérési értékként adja vissza. int Summation(const vector<vector<int> > &t, int k) { int n = (int)t.size(); int s = 0; for(int i=1; i<=n-abs(k); ++i){ s += t[(abs(k)-k+2*i)/2][(abs(k)+k+2*i)/2]; return s; Bemenő adatok beolvasása Egy mátrix szöveges állományból való feltöltése már többször szerepelt a korábbi alkalmazásokban. Ezt tartalmazza a ReadMatrix() eljárás, amelynek egyetlen bemenő és egyben eredmény paraméterváltozója a mátrixra való hivatkozás lesz. void ReadMatrix(vector<vector<int> > &t) 318

319 { ifstream f; string fname; bool hiba; do{ cout << "A mátrixot tartalmazó állomány:"; cin >> fname; f.open(fname.c_str()); if(hiba = f.fail()){ cout << "Hibás állománynév!\n"; f.clear(); while(hiba); int n; f >> n; t.resize(n); for(int i=0; i<n; ++i){ t[i].resize(n); for(int j=0; j<m; ++j) f >> t[i][j]; f.close(); 319

320 Először az állomány nevét olvassuk be, majd megpróbáljuk megnyitni az állományt. Ha nem sikerül, új állomány nevet kér a program. Ez tehát egy helyben lekezelt hibaeset. Ezután következik a mátrix méretének és tartalmának beolvasása. Ha nem tehetjük fel, hogy a szöveges állomány helyesen van kitöltve, akkor különféle hibaesetek fordulhatnak elő: 1. Nem egész számot olvasunk. 2. A mátrix mérete nem lehet negatív. 3. Az első két számot követő számok darabszáma kevesebb, mint az első két szám szorzata, esetleg már első két szám is hiányzik. Amennyiben ezeket a hibaeseteket a beolvasásnál csak észrevenni akarjuk, de a lekezelésüket (a hibákra történő reagálásokat) a főprogramra bízzuk, akkor a legegyszerűbb, ha kivételkezelést alkalmaznunk. Definiáljuk kivételekként a lehetséges hiba eseteket úgy, mint egy felsorolt típus értékeit. enum Errors{ Non_Integer, Negativ_Matrix_Size, Not_Enough_Number ; Ha valamelyik hibaeset bekövetkezik, akkor egy annak megfelelő kivételt dobunk, az eldobott kivételeket pedig a main függvényben kapjuk el és kezeljük le. vector<vector<int> > t; try{ ReadMatrix(t); catch(errors ex){ switch(ex){ 320

321 case Non_Integer: cout << "Nem egész szám!\n"; break; case Negativ_Matrix_Size: cout << "Negatív sor/oszlopok szám!\n"; break; case Not_Enough_Number: cout << "Hiányzó adatok a mátrixból!\n"; break; default:; exit(1); Módosítsuk a ReadMatrix() eljárást úgy, hogy ha valamelyik hibaeset bekövetkezik az olvasás során, akkor dobjon egy Errors típusú kivételt. Erre a lehetőségre utalhat az eljárás fejében a paraméterlista után elhelyezett throw (Errors) kifejezés, amely azt jelzi, hogy a függvény ilyen és csak ilyen típusú kivételt dobhat. Ez azonban igen nagy felelősséget kíván a programozótól, aki ezzel azt vállalja, hogy minden egyéb kivételt, amely ennek a kódrésznek a végrehajtásakor keletkezik, lekezel. void ReadMatrix(vector<vector<int> > &t) { ifstream f; bool error; string str; 321

322 do{ cout << "Fajl neve:"; cin >> str; f.open( str.c_str() ); if ( error = f.fail() ){ cout << "Nincs ilyen nevu fajl" << endl; f.clear(); while (error); int n = ReadIntFromFile(f); if(n<0) throw Negativ_Matrix_Size; t.resize(n); for(int i=0; i<n; ++i){ t[i].resize(n); for(int j=0; j<n; ++j){ t[i][j] = ReadIntFromFile(f); f.close(); A ReadMatrix() eljárás meghív egy segédfüggvényt is, amelyik egy egész számot próbál beolvasni, és olvasási hiba esetén ez is kivételeket dob. Ezeket a kivételeket a ReadMatrix() nem kapja el, nem kezeli le, hanem 322

323 automatikusan tovább dobja, így ezek végül a main függvény kivételkezelésében csapódnak le. int ReadIntFromFile(ifstream &f) { string str; f >> str; if( f.eof() ) throw Not_Enough_Number; int n = atoi(str.c_str()); if( 0 == n && str!= "0" ) throw Non_Integer; Tesztelés return n; Kezdjük most is az érvényes fekete doboz tesztesetekkel: 1. Intervallum tesztje: a. 0 0-s mátrix esetén a válasz az, hogy nincs. b. 1 1-s mátrix 0 elemmel. Válasz: van. c. 2 2-s mátrix jobb felső eleme 0, a többi 1. Válasz: van. d. 2 2-s mátrix bal alsó eleme 0, a többi 1. Válasz: van. 2. Lineáris keresés tesztje: a. 1 1-s mátrix 0, illetve nem 0 elemmel. b. 2 2-s mátrix fő átlójában +1 és -1, a többi elem +1. Válasz: van. c. 3 3-s csupa pozitív elemet tartalmazó mátrix. Válasz: nincs. 323

324 3. Összegzés tesztje: a. 3 3-s mátrix, a főátlón kívül minden elem pozitív, és a főátlóbeli nem nulla elemek összege nulla. Válasz: van. b. 3 3-s mátrix, a főátló feletti átlón kívül minden elem pozitív, és a főátló feletti átló nem nulla elemeinek összege nulla. Válasz: van. c. 3 3-s mátrix, a főátló alatti átlón kívül minden elem pozitív, és a főátló alatti átló nem nulla elemeinek összege nulla. Válasz: van. Ezek elegendőek a LinSearch() és az Summation() fehér doboz tesztelésére is. A ReadMatrix() tesztelését a fenti tesztesetek lefedik. Az érvénytelen adatok kivédését a ReadIntFromFile() függvény végzi. Tesztadatokat kell viszont generálni a kivételkezelés mindhárom esetére, ami lényegében a main()tesztelését jelenti. 1. Rossz formátumú számok a szöveges fájlban. 2. Negatív a mátrix mérete a szöveges fájlban. 3. Nincs kellő darabszámú elem a szöveges fájlban. 324

325 Teljes program #include <iostream> #include <fstream> #include <vector> #include <string> #include <cmath> #include <cstdlib> using namespace std; enum Errors{ Non_Integer, Negativ_Matrix_Size, Not_Enough_Number ; bool LinSearch(const vector<vector<int> > &t); int Summation(const vector<vector<int> > &t, int k); void ReadMatrix(vector<vector<int> > &t); int ReadIntFromFile(ifstream &f); int main() { // Mátrix létrehozása és kitöltése vector<vector<int> > t; try{ ReadMatrix(t); 325

326 catch(errors ex){ switch(ex){ case Non_Integer: cout << "Nem egész szám!\n"; break; case Negativ_Matrix_Size: cout << "Negatív sor/oszlopok szám!\n"; break; case Not_Enough_Number: cout << "Hiányzó adatok a mátrixból!\n"; break; default:; exit(1); // Eredmény kiírása int ind; if(linsearch(t,ind)) cout << "Minden átló elemösszege nulla.\n"; else cout << "Van nem zéróösszegű átló is.\n"; return 0; bool LinSearch(const vector<vector<int> > &t) 326

327 { int n = (int)t.size(); bool l = false; for(int k=1-n;!l && k<=n-1; ++k){ l = 0 == Summation(t,k); return l; int Summation(const vector<vector<int> > &t, int k) { int n = (int)t.size(); int s = 0; for(int i=1; i<=n-abs(k); ++i){ s += t[(abs(k)-k+2*i)/2][(abs(k)+k+2*i)/2]; return s; void ReadMatrix(vector<vector<int> > &t) { ifstream f; bool error; string str; do{ 327

328 cout << "Fajl neve:"; cin >> str; f.open( str.c_str() ); if ( error = f.fail() ){ cout << "Nincs ilyen nevu fajl" << endl; f.clear(); while (error); int n = ReadIntFromFile(f); if(n<0) throw Negativ_Matrix_Size; t.resize(n); for(int i=0; i<n; ++i){ t[i].resize(n); for(int j=0; j<n; ++j){ t[i][j] = ReadIntFromFile(f); f.close(); int ReadIntFromFile(ifstream &f) { 328

329 string str; f >> str; if( f.eof() ) throw Not_Enough_Number; int n = atoi(str.c_str()); if( 0 == n && str!= "0" ) throw Non_Integer; return n; 329

330 C++ kislexikon kivétel kivétel dobása kivételt dobó függvény kivétel figyelése tetszőleges érték vagy objektum throw kivétel int fv( ) throw (kivétel típusa) try{ kivétel elkapása és kezelése catch(típus változó){ if(kivétel==változó) 330

331 9. Fordítási egységekre bontott program Egy összetett program kódja jóval áttekinthetőbbé válik, ha a logikailag összetartozó részeket (alprogramokat, azok által közösen használt változókat, konstansokat, típusdefiníciókat) külön egységként, úgynevezett modulokban írjuk le. Tulajdonképpen egy kommentekkel és üres sorokkal elhatárolt kódrészt is lehet ilyen külön egységnek tekinteni, de egy alprogramot már minden ellenérzés nélkül önálló modulként emlegethetünk. Modul az is, ha több, valamilyen szempont szerint összetartozó alprogramot gyűjtünk össze, sőt egy ilyen modul kiegészülhet az alprogramjai által közösen használt típusok, konstansok és változók definícióival. Ha ezeknek a valamilyen szempont szerint összegyűjtött alprogramoknak, típusoknak, változóknak és konstansoknak a gyűjteményét önálló fordítási egységben írjuk le, akkor a modult csomagnak nevezzük. Egy program csomagokra bontása számos előnnyel jár. Egy csomag a program többi része nélkül fordítható, sőt megfelelő keret programmal önállóan tesztelhető. Ez lehetőséget teremt arra, hogy egy program egyes csomagjait más-más programozó egymással párhuzamosan fejleszthesse és így a program csoportmunkában készüljön, valamint hogy ugyanazt a csomagot több különböző programban is felhasználhassunk. A program egyes részéinek egymástól való fizikai elkülönítése erősíti az egyes részeken belüli összetartozást, még átláthatóbbá teszi az egyes részek közötti kapcsolatokat. Egy csomag felhasználása azt jelenti, hogy a benne definiált elemeket közvetve vagy közvetlenül igénybe vesszük abban a programban, amelynek ez a csomag a része. Ehhez természetesen valahogy jelölni kell valahogyan azt, ha a program egy szakaszában használni kívánjuk egy csomag elemeit. Egy csomag leírása magába foglalja a benne definiált típusoknak, változóknak, konstansoknak és alprogramoknak a leírását, azaz a csomag törzsét. Meg kell adni azt is, hogy melyek azok az elemek, amelyeket a csomagon kívül is közvetlenül használhatunk (mit szolgáltat, exportál a csomag), melyek azok, amelyek csak belső (csomagon belüli) használatra 331

332 készültek. Magában a csomag törzsében lehetnek olyan részek, amelyek más csomagok szolgáltatásait igénylik, importálják. Implementációs stratégia A kód modulokra bontása, majd bizonyos moduljainak önálló fordítási egységbe, azaz csomagba zárása kellő tapasztalatot igényel. Csak sok program megírása után alakul ki egy programozóban az a készség, hogy milyen logika mentén érdemes egy csomagot kialakítani. Az egyik rendező elv az azonos célú, hasonló funkciójú alprogramok egybegyűjtése. Például egy csomagba tehetünk különféle adatbeolvasó eljárásokat, amelyek közül az egyik egy egész számot, a másik egy valós számot, a harmadik egy tömböt, stb. olvashat be. Külön csomagba kerülhetnek ezen eljárásoknak a billentyűzetről olvasó, külön csomagba a szöveges állományból olvasó változatai. Egy másik példa az, amikor bonyolult matematikai számításokat végző eljárásokat (mondjuk lineáris egyenletrendszerek különféle megoldásait vagy integrálszámító numerikus módszereket) gyűjtünk egy csomagba. Funkció vezérelt: amikor a hasonló funkciójú alprogramok alkotnak egy csomagot. Típus központú: amikor egy adattípus lehetséges műveleteit megvalósító alprogramok alkotnak egy csomagot ábra. Csomagok kialakításának elvei Másik rendező elv az, amikor egy bizonyos adattal kapcsolatos műveleteket gyűjtjük össze az adat elemeit tartalmazó változókkal együtt. Egy ilyen csomag az adat egyes elemeinek elérését és az azokkal dolgozó műveleteket biztosítja a külvilág számára, azaz magát az adat típusát írja le abban az értelemben, ahogy adattípuson az adat lehetséges értékeinek halmazát és az azokon értelmezett műveleteket értjük. Egy ilyen leírásra az objektum orientált programozási nyelvek osztály fogalma a legalkalmasabb 332

333 (ezt a 11. fejezetben mutatjuk be), de egy kezdetleges próbálkozást már e fejezet 23. feladatának megoldásában is mutatunk. A csomag az alprogramjain kívül tartalmazhat azok által közösen használt típus-, változó- és konstans, továbbá úgynevezett belső modulokat is. Éppen ezért nagyon alaposan meg kell azt gondolni, hogy egy csomag mely elemeit lehessen a csomagon kívül használni, és amelyek legyenek azok, amelyek csak belső használatra szolgálnak. Különösen kényes kérdés a csomagbeli változók külső használatának engedélyezése. Ezek ugyanis olyan globális változók, amelyek a csomagban is láthatóak és a program azon részén is, ahol a csomagot használjuk, de nem láthatók más csomagokban. A globális változók használatával szemben már eddig is megfogalmaztuk a fenntartásainkat, és ezen most sem kívánunk változtatni. Export lista: A komponens által nyújtott szolgáltatások, a komponensen kívül használható konstansok, típusok és meghívható alprogramok felsorolása. Import lista: A komponens által használt más komponensekben definiált szolgáltatások felsorolása. Törzs: A komponens szolgáltatásainak megvalósítása ábra. Egy komponens részei Ha egy csomag azokról a szolgáltatásairól, amelyeket az őt használó programrészek számára nyújt, pontos leírással rendelkezik, azaz szolgáltatás központú szemlélet alapján készült, akkor a csomagot komponensnek szokás hívni. Fogalmi szempontból nem túl nagy a különbség a komponens és a csomag között. Nyelvi szempontból ott húznám meg a határt, amikor lehetőség van közvetlen módon megadni egy komponens export és import listáját. A C++ nyelvi eszközei csak közvetett módon teszik ezt, ezért mi inkább a csomag kifejezést használjuk. A komponens interfésze (export listája) sorolja fel a komponens igénybe vehető szolgáltatásait. Ezek csomagon kívül használható változók, 333

334 típusok, konstansok és alprogramok. A komponens implementációja (törzse) e szolgáltatások biztosításához szükséges exportált és nem exportált elemek kódját tartalmazza. Egy komponens maga is támaszkodhat más komponensek szolgáltatásaira, használhat máshol definiált változókat, konstansokat, típusokat, alprogramokat, azaz rendelkezik igényeinek, szükségleteinek gyűjteményével (import listával). Nyelvi elemek A több csomagra bontott C++ kód a main függvényt tartalmazó főforrásállományon (alapértelmezésben main.cpp) kívül tetszőleges számú forrásállományból állhat, amelyek egy-egy csomag törzsét tartalmazzák. Minden ilyen forrásállományhoz létre szoktak hozni egy úgynevezett fejállományt is, amely az adott csomag interfészének (exportlistájának) megadására szolgál. Így egy csomag tulajdonképpen két állományban, egy fej- és egy forrásállományban helyezkedik el. A fejállományba a csomag azon elemei (típus definíciók, alprogram deklarációk, konstans definíciók, változók, és olyan összetettebb belső modulok definíciói is, mint az osztályok) kerülnek, amelyeket exportálni szeretnénk. A csomag minden egyéb része (alprogramok és egyéb típusok definíciója, a csomagra nézve globális változók) a forrásállományba kerül. Bár nem kötelező, de célszerű az összetartozó fej- és forrásállomány nevét ugyanannak választani, és csak az állománynév kiterjesztésével megkülönböztetni. A forrásállomány egy önmagában is lefordítható C++ kódot tartalmaz, ezért a kiterjesztése cpp. A fejállomány viszont nem fordítható önállóan, ezt azzal jelezzük, hogy a kiterjesztése cpp helyett h. A fejállományt mindig bemásoljuk (#include) a hozzá tartozó forrásállományba, hiszen a csomag testének is ismernie (látnia) kell önmaga interfészét. De bemásoljuk a csomag fejállományát a program minden olyan állományába is, ahol a C++ kód hivatkozik a fejállományban felkínált valamelyik szolgáltatásra, azaz importálni kívánja azokat. Egy forrásállományba egy fejállomány nemcsak közvetlen másolással kerülhet be, hanem közvetve is egy olyan másik fejállomány bemásolásakor, amely az első fejállományt bemásolja. Elkerülendő, hogy ezáltal ugyanaz a 334

335 fejállomány többszörösen bemásolódjon egy kódba, mert a fordító program ilyenkor hibát jelez. Ezért a bemásolásokat végző előfordítót úgynevezett állomány-őrszemekkel szokták befolyásolni. A fejállományok első sorába az #ifndef NEV utasítást (a NEV -nek ajánlott a fejállomány fizikai nevét választani), a második sorba a #define NEV utasítást, és az utolsó sorba a #endif utasítást írjuk. A forrásállományok (és ez vonatkozik a fő-forrásállományra is) rendszerint függvénydefiníciókat tartalmaznak, de megjelenhetnek benne csak az adott forrásállományra érvényes típusdefiníciók, konstansok, és ritkán (globális) változók is. Ilyenkor a forrásállománynak az elején helyezzük el a típus-, konstans- és változó-definíciókat, majd a kizárólag ebben az állományban használt függvények deklarációit, végül ezt követik a függvények definíciói, de nem csak a belső függvényeké, hanem a forrásállományhoz esetleg hozzátartozó fejállományban deklarált függvényeké is. A fő-forrásállományban elsőként a main függvényt szokás definiálni. Ezt a konvenciót már eddig is használtuk az egyetlen forrásállományból álló programjainkban. A csomagok önálló fordítási egységek, azaz egymástól függetlenül lehet őket fordítani. Egy programmá a szerkesztés során forrnak össze. Továbbra is igaz az, hogy a C++ programnak összességében egy main függvénnyel kell rendelkezni ahhoz, hogy futtatható legyen. Integrált fejlesztő eszközök használatakor a csomagokat közös projektbe szokás összerakni, innen fogja tudni a futtató környezet, hogy mely lefordított csomagokat kell egy programmá összeszerkeszteni és futtatni. Ha egy csomag forrásállományában olyan globális változót szeretnénk használni, amelyet kizárólag belső elemként, csak a csomag alprogramjai számára akarunk elérhetővé tenni, akkor ezt a szándékunkat a static kulcsszó feltüntetésével jelezhetjük. Ennek használata nélkül más csomagok számára is láthatóvá tehető egy csomag globális változója, amennyiben annak deklarációját a másik csomagban az extern kulcsszó megadása után megismételjük. Ha egy mód van rá, ne használjuk ezt a lehetőséget. (Ha mégis szükség lenne olyan változókra, amelyeket több csomagban is használni szeretnénk, akkor deklaráljuk azokat egy külön fejállományban, amelyet az összes érintett csomag forrásállományában másoljunk be.) 335

336 Csomagokat eddigi programjainkban is használtunk már, de azokat nem mi készítettük el, hanem készen álltak a rendelkezésünkre. Gondoljunk az iostream, fstream, string, vector, math.h csomagokra, amelyek szolgáltatásait (típusait, függvényeit) igénybe vettük, azok a programunk részei lettek, de a fejlesztésükkel nem kellett foglalkoznunk. A névterek használatával rugalmasan lehet a programunk azonosítóinak hatáskörét (láthatóságát) befolyásolni. Használatuk lehetővé teszi, hogy azonos nevű, de eltérő jelentésű azonosítókat definiáljunk, amelyeket az őket tartalmazó névtér nevének segítségével minősítve különböztethetünk meg. Ehhez elég az azonosító neve elé írni a definíciót tartalmazó névtér nevét és a :: szimbólumot. A minősítés elhagyható a kód azon részén, amely előtt a using namespace <névtérnév> utasítás található. A standard névtér elemeit használó forrásállományok elején mindig célszerű a using namespace std utasítást elhelyezni. A fejállományokba ellenben nem szokás ezt beírni, ezért ha hivatkozunk a standard névtér elemeire, akkor azokat csak az std:: minősítéssel tudjuk használni. 336

337 22. Feladat: Műkorcsolya verseny Soroljuk fel azokat a műkorcsolyázó versenyzőket, akik holtversenyben az első helyen végeztek a kötelező gyakorlatuk bemutatása után. Az n versenyző programját m tagú zsűri pontozta, amelyből egy versenyző összesített pontszámát úgy kapjuk, hogy a legjobb és legrosszabb pontot elvéve a többi pontszám átlagát képezzük. (A feladat megtalálható az első kötet 6.9. példájaként.) Specifikáció A = ( t : R n m, s : N * ) Ef = ( t=t n 1 m>2 ) Uf = ( Ef s = i Absztrakt program n i 1 pont( i) max ahol pont:[1..n] R pont(i) = ( m i 1 n max = max pont(i) ) m i 1 t [ i, j] max t[ i, j] min t[ i, j] )/(m 2) A megoldás egy kiválogatás, azaz egy olyan összegzés, ahol az összeadás helyett az összefűzés műveletét használjuk, és amely csak bizonyos tulajdonságú elemeket ír bele az s eredmény-sorozatba. j 1 m j 1 max, p inicializálás p:r n, max:r s := <> i = 1..n i:n max=p[i] s := s <i> SKIP 337

338 Ezt megelőzi a versenyzők összesített pontszámait egy tömbben elhelyező és közben ezek közül a legnagyobb összesített pontszámot is meghatározó előkészítő rész (max, p inicializálása), amely egy összegzés (valójában egy összefűzés) és egy maximum kiválasztás ciklusainak összevonásaként állt elő. max, p inicializálás p[1] := pont(1) max := p[1] d := pont(i) o,maxi,mini := t[i,1], t[i,1], t[i,1] j = 2..m maxi, mini, o:r i = 2..n i:n o := o+t[i,j] j:n p[i]:=pont(i) t[i,j]>maxi p[i]>max maxi:= t[i,j] SKIP max:= p[i] SKIP t[i,j]<mini mini:= t[i,j] SKIP d := (o-maxi-mini)/(m-2) Egy versenyző összesített pontszámát egy maximum és egy minimum kiválasztással módosított összegzés (d:=pont(i)) számítja ki. Implementálás A megoldó programot két részre bontjuk. Az egyikbe, ez a main.cpp, az absztrakt program kódja kerül, amelyben külön alprogramot alkot majd a max,p:=inicializálás (Init()), a d:=pont(i) (Point()) valamint az absztrakt főprogram (Select()). Külön csomagot (matrix.cpp) képez egy mátrixot egy szöveges állományból feltöltő alprogram (Fill()) és egy mátrixot a konzolablakba kiíró alprogram (Write()). Itt a csomagokra bontásnál az úgynevezett funkció vezérelt modularizálási technikát alkalmazzuk, nevezetesen a 338

339 valamilyen értelemben rokon funkciójú (itt mátrix beolvasását és kiírását végző) függvényeket gyűjtjük egy csomagba. A tervben szereplő tömbök (t, p) a kódban 0-tól indexelt tömbök lesznek, ennek megfelelően a tömböket feldolgozó számlálós ciklusok indextartománya is eggyel eltolódik az absztrakt programokhoz képest. A program kerete A vezérlést, azaz az alprogramok megfelelő sorrendben történő meghívását a main függvény biztosítja. int main() { vector<vector<double> > t; Fill(t); if(t.size()<1 or t[i].size()<3){ cout << "Nem jó méretű a mátrix\n"; return 1; cout << "Pontszamok: \n"; Write(t); vector<double> p(t.size()); double max; Init(t,p,max); Select(p,max); 339

340 return 0; Függvények hívási láncolata main() Fill() Write() Init() Select() ReadIntFromFile() ReadRealFromFile() Result() 9-3. ábra. Alprogramok hívási lánca A main függvény kapja meg a Fill() függvénytől a pontszámokat tartalmazó mátrixot (t), amelyet odaad egyrészt a Write()-nak, hogy az kiírja, másrészt az Init()-nek, hogy az feldolgozza. Az Init() egyrészt kiszámolja az egyes versenyzők összpontszámait (ehhez meghívja Result() függvényt a mátrix egy-egy sorára) és azokat a p tömbben helyezi el, másrészt meghatározza a maximális összpontszámot (max). Mindkét adatot visszaadja a main függvénynek, amely továbbadja azokat a Select()-nek. A Fill() függvény két segédfüggvényt használ a szöveges állományból való egész illetve valós számok ellenőrzött olvasásához. Komponens szerkezet A függvényeket két csomagban helyezzük el. A mátrix feltöltését és kiírását végző műveleteket a matrix csomagba (matrix.cpp), a többi függvényt a fő programba (main.cpp). 340

341 main.cpp matrix.h - matrix.cpp main() Init() Select() Result() Fill() Write() ReadIntFromFile ReadRealFromFile Absztrakt program kódolása 9-4. ábra. Komponens szerkezet Az absztrakt programnak megfeleltetett kód a main függvényben, illetve az abból meghívott Init(), Result() és Select() függvényekben található. Az Init() bemenő paramétere a valós számokat tartalmazó t mátrix (erre utal a konstans hivatkozás szerinti paraméter), eredmény paraméterei a valós számokat tartalmazó p tömb és a max valós szám (mindkettő hivatkozás szerinti paraméter). void Init(const vector<vector<double> > &t, vector<double> &p, double &max) { p[0] = Result(t[0]); max = p[0]; for(int i=1; i<(int)t.size(); ++i){ p[i] = Result(t[i]); if(p[i]>max) max = p[i]; 341

342 A Result() bemenő adata a mátrix egy sora, visszatérési értéke egy valós szám. A tervben szereplő két elágazás szekvenciája átalakítható egy háromágú elágazássá (a harmadik ág üres) hiszen a v[j]>max és a v[j]<min feltételek egymást kizárják. double Result(const vector<double> &v) { double o, maxi, mini; o = maxi = mini = v[0]; for(int j=1; j<(int)v.size(); ++j){ o = o + v[j]; if(v[j]>maxi) maxi = v[j]; else if(v[j]<mini) mini = v[j]; return (o-maxi-mini)/(v.size()-2); A Select()a p valós számokat tartalmazó tömb és a max valós számot kapja bemenetként. void Select(const vector<double> &p, double max) { cout << "A legjobb versenyzők:\n"; for(int i=0; i<(int)p.size(); ++i){ 342

343 if(p[i] == max) cout << i+1 << "\t"; Mátrix csomag Ez a csomag egy mátrix szöveges állományból történő feltöltését és kiírását biztosítja. A csomag szolgáltatásait (a Fill() és a Write() alprogramok deklarációit) a matrix.h fejállomány sorolja fel. Ez tehát az export lista. Mivel itt a vector<> típust használjuk, ezért hivatkoznunk kell a könyvtári vector csomagra is. Ez a csomagunk import listája. A fejállományokban nem szokás a using namespace std utasítást használni, ezért, mivel a vector az std (standard névtér) eleme, a vector azonosítója elé ki kell írni az std:: minősítést. A fejállományt megfelelő őrszem-utasítással kell ellátni. #ifndef MATRIX_H #define MATRIX_H #include <vector> void Fill( std::vector<std::vector<double> > &t); void Write( #endif const std::vector<std::vector<double> > &t); Ezt a fejállományt inklúdolja majd mind a main.cpp, ahonnan az alprogramokat meghívjuk, mind a matrix.cpp, ahol az alprogramok 343

344 definíciója található. A Fill() eredmény paramétere és a Write() bemenő paramétere egyaránt a valós számokat tartalmazó mátrix. A komponensünk törzsét, a Fill() és a Write() alprogramok kódját a matrix.cpp tartalmazza. A mátrix beolvasásánál (Fill()) feltesszük, hogy az adatokat tartalmazó szöveges állományban először két természetes számot (sorok és oszlopok száma) találunk, majd azokat követően annyi darab valós számot (mátrix elemei), amennyi a két természetes szám szorzata. A számokat legalább egy elválasztó jel (szóköz, tabulátor jel vagy sorvége jel) határolja. Ez a forma lehetővé teszi például azt, hogy az állomány első sorában a sorok és oszlopok számát adjuk meg, majd soronként a mátrix egyes soraihoz tartozó elemeket. void Fill(vector<vector<double> > &t) { ifstream f; bool hiba; string str; do{ cout << "Fájl neve:"; cin >> str; f.open( str.c_str() ); if ( hiba = f.fail() ){ cout << "Nincs ilyen nevű fájl" << endl; f.clear(); while (hiba); 344

345 int n, m; f >> n >> m; t.resize(n); for(int i=0; i<n; ++i){ t[i].resize(m); for(int j=0; j<m; ++j) f >> t[i][j]; f.close(); Módosítsuk a mátrixnak szöveges állományból történő feltöltését úgy, hogy az olvasásnál bekövetkező esetleges hibákat is figyeljük, és erről a feltöltést meghívó programot kivétel dobásával értesítjük. Az előforduló hibaesetek: 1. Nem egész számot olvasunk. 2. Nem valós számot olvasunk. 3. A mátrix mérete nem lehet negatív. 4. Az első két számot követő számok darabszáma kevesebb, mint az első két szám szorzata, esetleg már első két szám sincs. Nem ide tartozik az a hiba, amikor nincs legalább egy sora és három oszlopa a mátrixnak. Ezt ugyanis a konkrét feladat előfeltétele írja elő, tehát a főprogramban és nem egy általános mátrix-beolvasó eljárásban kell vizsgálni. Ugyancsak nem ide soroljuk a nem létező állománynév hibaesetet, mert ezt helyben, az olvasáson belül kezeljük. A kivételek típusát a matrix.h-ban definiáljuk. Ez bármi lehet: legegyszerűbb esetben a hibaeset megnevezése (ami lehet egy sztring vagy egy felsorolt típus eleme), összetettebb esetben egy több adattagot is 345

346 hordozó objektum. Mi itt szeretnénk kivételként visszaadni a hibaeset megnevezését és bizonyos esetekben magát a beolvasott hibás adatot is. Ezért a kivétel egy rekord (struct) lesz, amelynek egyik tagja a hibaeset neve, amit egy felsorolt típus értékei közül választhatunk, másik tagja pedig egy üzenet (string). Négy hibaesetet különböztetünk meg a beolvasás során előforduló négy féle hiba miatt. enum Errors{Non_Integer, Non_Real, Negativ_Matrix_Size, Not_Enough_Number; struct Exceptions{ Errors code; std::string msg; ; Egészítsük ki a Fill() eljárást úgy, hogy ha valamelyik hibaeset bekövetkezik az olvasás során, akkor dobjon egy kivételt. Ez arra kényszeríti a komponens használóját (esetünkben a main függvényt), hogy készüljön fel mindenféle Exceptions típusú kivételt lekezelésére. Az alprogram eleje nem változik: void Fill(vector<vector<double> > &t) { ifstream f; bool hiba; string str; do{ cout << "Fájl neve:"; 346

347 cin >> str; f.open( str.c_str() ); if ( hiba = f.fail() ){ cout << "Nincs ilyen nevű fájl" << endl; f.clear(); while (hiba); Egy kivételnek ki kell tölteni a hibakód- és az üzenet mezőjét. Az üzenetbe célszerű beleírni azt az adatot is, amely a kivétel dobását kiváltotta. Esetünkben az adatok számok, amelyeket sztringgé kell konvertálnunk. Az alábbi kódban ezt egy ostringstream típusú objektum segítségével végezzük el. Ehhez szükségünk van az sstream csomagra. int n = ReadIntFromFile(f); if(n<0) { Exceptions ex; ex.code = Negativ_Matrix_Size; ostringstream ss; ss << n; ex.msg = ss.str(); throw ex; int m = ReadIntFromFile(f); if(m<0){ 347

348 Exceptions ex; ex.code = Negativ_Matrix_Size; ostringstream ss; ss << m; ex.msg = ss.str(); throw ex; t.resize(n); for(int i=0; i<n; ++i){ t[i].resize(m); for(int j=0; j<m; ++j){ t[i][j] = ReadRealFromFile(f); f.close(); A Fill() eljárás meghív két segédfüggvényt is: ReadIntFromFile() és ReadRealFromFile(). Egyik egy egész, másik egy valós számot próbál beolvasni. Ezekre a segédfüggvényekre a főprogramban nincs közvetlenül szükség, a matrix komponens nem is ajánlja fel őket külön szolgáltatásként, hiszen nem szerepelnek az export listában, azaz a matrix.h-ban. Ezek a függvények is dobnak Exceptions típusú kivételt, amelyet a Fill() nem kezel le, hanem tovább dob. int ReadIntFromFile(ifstream &f) 348

349 { string str; f >> str; if( f.eof() ){ Exceptions ex; ex.code = Not_Enough_Number; throw ex; int n = atoi(str.c_str()); if( 0 == n && str!= "0" ){ Exceptions ex; ex.code = Non_Integer; ex.msg = str; throw ex; return n; Olvasási hiba esetén mindkét segédfüggvény kivételeket dobhat, amelyeket azonban a Fill() nem kezel le, hanem automatikusan továbbdobja az őt hívó programrésznek. Ezek éppen olyan Exception típusú kivételek, mint amelyeket a Fill() amúgy is dobni tud. double ReadRealFromFile(ifstream &f) { 349

350 string str; f >> str; if( f.eof() ){ Exceptions ex; ex.code = Not_Enough_Number; throw ex; double a = atof(str.c_str()); if( 0 == a && str!= "0" ){ Exceptions ex; ex.code = Non_Real; ex.msg = str; throw ex; return a; A main függvényben kell elkapnunk és lekezelnünk a Fill() által dobható kivételeket. Ott ahol a kivétel adatat is lényeges, nemcsak a hibaeset nevát, de az üzenetét is kiírjuk. Láthatjuk, hogy külön ellenőrizzük, hogy a mátrix legalább 1 3-as legyen. vector<vector<double> > t; try{ Fill(t); 350

351 catch(exceptions ex){ switch(ex.code){ case Non_Integer: cout << "Rossz formájú egész szám!" << ex.msg << endl; break; case Non_Real: cout << "Rossz formájú valós szám!" << ex.msg << endl; break; case Negativ_Matrix_Size: cout << "Sor, oszlop szám nem negatív!" << ex.msg << endl; break; case Not_Enough_Number: cout << "Hiányzó adatok!\n"; break; default:; exit(1); if(t.size()<1){ cout<<"legalább 1 versenyző kell!"; exit(1); if(t[0].size()<3){ cout<<"legalább 3 zsűritag kell"; exit(1); 351

352 A csomag másik eleme, a Write(), egy valós számokat tartalmazó mátrixot ír ki. void Write(const vector<vector<double> > &t) { Tesztelés for(int i=0; i<(int)t.size(); ++i){ for(int j=0; j<(int)t[i].size(); ++j){ cout << t[i][j] <<"\t"; cout << endl; Fekete doboz tesztesetek: (Az egyes programozási tételek tesztjét összevonjuk a tételekben szereplő intervallum tesztjével. Az egyes esetek ezen túlmenően is átfedik egymást. Megjegyezzük, hogy segítené a tesztelést, ha a kiválogatott versenyzők pontszámát is kiírná a programunk.) 1. Kiválogatás tesztje: a. 1 versenyző és 3 zsűritag. Válasz: <1>. b. Több versenyző azonos pontszámokkal. Válasz: minden versenyző <1, 2,, n>. c. Több versenyző közül az első a legjobb. Válasz: <1>. d. Több versenyző közül az utolsó a legjobb. Válasz: <n>. e. Több versenyző közül minden második a legjobb. Válasz: <2, 4, 6, >. 2. Az Init()-beli maximum kiválasztás tesztje: a. 1 versenyző. Válasz: <1>. 352

353 lásd még fenti esetek közül: b és c-t. 3. Az Result()-beli összegzés tesztje: a. 1 versenyző és 3 zsűritag. Válasz: <1>. b. 1 versenyző és 5 zsűritag. Válasz: <1>. 4. Az Result()-beli maximum kiválasztás tesztje: 5. Az Result()-beli maximum kiválasztás tesztje: a. 1 versenyző és 3 zsűritag, és az első adta a legtöbb pontot. Válasz: <1>. b. 1 versenyző és 3 zsűritag, és az utolsó adta a legtöbb pontot. Válasz: <1>. c. 1 versenyző és 3 zsűritag, és az első kettő adta a legtöbb pontot. Válasz: <1>. d. 1 versenyző és 3 zsűritag, és az utolsó kettő adta a legtöbb pontot. Válasz: <1>. e. 1 versenyző és 3 zsűritag, és mind ugyanannyi pontot adott. Válasz: <1>. 6. Az Result()-beli minimum kiválasztás tesztje a 6. pont alapján. Modulonkénti fehér doboz tesztelés: A fenti esetek elegendőek az Init(), a Result() és a Select() tesztelésére. Fill() tesztelése: 1. Nem létező bemenő állomány esete. 2. Egész számok helyett más adatok a szöveges állományban. 3. Valós számok helyett más adatok (betű, sztring) a szöveges állományban. 4. A szöveges állományban megadott darabszámnál kevesebb adat van a fájlban. Write() tesztelése: a korábbi tesztesetek lefedik ennek a modulnak a tesztelését. 353

354 354

355 Teljes program main.cpp: #include <iostream> #include <vector> #include <cstdlib> #include "matrix.h" using namespace std; void Init(const vector<vector<double> > &t, vector<double> &p,double &max); void Select(const vector<double> &p, double max); double Result(const vector<double> &v); int main() { vector<vector<double> > t; try{ Fill(t); catch(exceptions ex){ switch(ex.code){ case Non_Integer: cout << "Rossz formájú egész szám!" 355

356 << ex.msg << endl; break; case Non_Real: cout << "Rossz formájú valós szám!" << ex.msg << endl; break; case Negativ_Matrix_Size: cout << "Sor, oszlop szám nem negatív!" << ex.msg << endl; break; case Not_Enough_Number: cout << "Hiányzó adatok!\n"; break; default:; exit(1); if(t.size()<1){ cout<<"legalább 1 versenyző kell!"; exit(1); if(t[0].size()<3){ cout<<"legalább 3 zsűritag kell"; exit(1); cout << "Pontszamok: \n"; 356

357 Write(t); vector<double> p(t.size()); double max; Init(t,p,max); Select(p,max); return 0; void Init(const vector<vector<double> > &t, vector<double> &p, double &max) { p[0] = Result(t[0]); max = p[0]; for(int i=1; i<(int)t.size(); ++i){ p[i] = Result(t[i]); if(p[i]>max) max = p[i]; void Select(const vector<double> &p, double max) { cout << "A legjobb versenyzők:\n"; for(int i=0; i<(int)p.size(); ++i){ if(p[i] == max) cout << i+1 << "\t"; 357

358 double Result(const vector<double> &v) { double o, maxi, mini; o = maxi = mini = v[0]; for(int j=1; j<(int)v.size(); ++j){ o = o + v[j]; if(v[j]>maxi) maxi = v[j]; else if(v[j]<mini) mini = v[j]; return (o-maxi-mini)/(v.size()-2); 358

359 matrix.h: #ifndef MATRIX_H #define MATRIX_H #include <vector> #include <string> enum Errors{Non_Integer, Non_Real, Negativ_Matrix_Size, Not_Enough_Number; struct Exceptions{ Errors code; std::string msg; ; void Fill( std::vector<std::vector<double> > &t); void Write( #endif const std::vector<std::vector<double> > &t); matrix.cpp: #include "matrix.h" #include <iostream> #include <cstdlib> #include <fstream> 359

360 #include <sstream> using namespace std; int ReadIntFromFile(ifstream &f); double ReadRealFromFile(ifstream &f); void Write(const vector<vector<double> > &t) { for(int i=0; i<(int)t.size(); ++i){ for(int j=0; j<(int)t[i].size(); ++j){ cout << t[i][j] <<"\t"; cout << endl; void Fill(vector<vector<double> > &t) { ifstream f; bool hiba; 360

361 string str; do{ cout << "Fájl neve:"; cin >> str; f.open( str.c_str() ); if ( hiba = f.fail() ){ cout << "Nincs ilyen nevű fájl" << endl; f.clear(); while (hiba); int n = ReadIntFromFile(f); if(n<0) { Exceptions ex; ex.code = Negativ_Matrix_Size; ostringstream ss; ss << n; ex.msg = ss.str(); throw ex; int m = ReadIntFromFile(f); if(m<0){ Exceptions ex; 361

362 ex.code = Negativ_Matrix_Size; ostringstream ss; ss << m; ex.msg = ss.str(); throw ex; t.resize(n); for(int i=0; i<n; ++i){ t[i].resize(m); for(int j=0; j<m; ++j){ t[i][j] = ReadRealFromFile(f); f.close(); int ReadIntFromFile(ifstream &f) { string str; f >> str; if( f.eof() ){ 362

363 Exceptions ex; ex.code = Not_Enough_Number; throw ex; int n = atoi(str.c_str()); if( 0 == n && str!= "0" ){ Exceptions ex; ex.code = Non_Integer; ex.msg = str; throw ex; return n; double ReadRealFromFile(ifstream &f) { string str; f >> str; if( f.eof() ){ Exceptions ex; ex.code = Not_Enough_Number; throw ex; 363

364 double a = atof(str.c_str()); if( 0 == a && str!= "0" ){ Exceptions ex; ex.code = Non_Real; ex.msg = str; throw ex; return a; 364

365 23. Feladat: Melyikből hány van Olvassunk be a billentyűzetről egész számokat, majd mondjuk meg, melyik szám hányszor szerepelt. Specifikáció A billentyűzetről érkező egész számok sorozatára a specifikációban a t változóval hivatkozunk. Célunk egy olyan s sorozat, egy úgynevezett tároló előállítása, amely érték-darabszám párokat tartalmaz, ahol egy érték egy t- beli elem, a hozzátartozó darabszám pedig annak t-beli előfordulási száma. Ugyanaz a t-beli elem csak egyszer szerepelhet az s-ben. A = ( t : Z *, s : Pár * ) Pár = rec(érték : Z, darab : N) Ef = ( t=t ) t' Uf = ( Ef s = t' k ) k 1 ahol művelet befűz az s sorozatba egy e egész számot úgy, hogy ha az már szerepel az s-ben (ehhez egy lineáris keresést kell az s értékeire alkalmazni), akkor csak annak darabszámát növeli meg, ha még nem, akkor felveszi az s- be 1 darabszámmal. Itt kibontakozik egy s := s specifikációja: A = ( s : Pár *, e : Z ) Ef = ( s=s e=e ) s' Uf = (e=e l, ind search ( si. érték i 1 e) e részfeladat, amelynek ( l s ind.darab = s ind.darab+1) ( l s = s <n,1>) ) Absztrakt program s := <> k = 1.. t k:n s := s t k 365

366 s:=s e l, i := hamis, 1 l :, i:n l i s l := s i.érték = e ind := i ind:n i := i+1 l s ind.darab := s ind.darab+1 s := s <e,1> Implementálás A megoldó programot két részre bontjuk. Az egyik az absztrakt főprogram lesz, a másik a tároló és a kapcsolódó műveleteinek csomagja. Itt tehát az úgynevezett típus központú modularizálási technikát alkalmazzuk, amikor egy adott típusú objektumot (esetünkben az s tárolót) közösen használó függvényeket, egy objektum műveleteit gyűjtjük külön csomagba, és ahol magát a közösen használt objektumot is definiáljuk, de annak közvetlen elérését nem engedjük a csomagon kívülről. Függvények hívási láncolata main() Store() Write() 9-5. ábra. Alprogramok hívási lánca 366

367 A main függvény az absztrakt főprogramot tartalmazza, amely a billentyűzetről olvassa a képzeletbeli t sorozat elemeit, elhelyezi azokat az s- ben a Store() segítségével, majd kiírja az s tartalmát a Write()-tal. A Store() valósítja meg az s:=s e alprogramot, a bemenetként kapott e elemet elhelyezi az s sorozatban. A Write() az s elemeinek a konzolablakra történő kiírását végzi. Komponens szerkezet A függvényeket két csomagban helyezzük el. Külön csomagot alkot a main függvény ez lesz a main.cpp-ben, és külön csomagba kerül a tárolót megvalósító kód. Mivel magát az s tárolót nem exportáljuk, a container.h fejállomány ezért csak a két művelet (Store() és Write()) deklarációját tartalmazza. Az s típusának definíciója és a két művelet alprogramjának törzse a container.cpp állományba kerül. main.cpp container.h - container.cpp main() Store() Write() 9-6. ábra. Komponens szerkezet A container csomag így egyetlen tárolót definiál csak. Ez nem alkalmas arra, hogy a leírását típusként értelmezve, több ilyen típusú tárolót hozzunk létre. (Ezt majd csak az osztály segítségével tudjuk megtenni.) Most azonban egyetlen tároló is elég. 367

368 Főprogram A main függvény az absztrakt főprogramot kódolja: int main() { cout << "Adjon meg egész számokat!\n int n; << "Betűvel jelezze a bevitel végét!\n"; while(cin >> n){ Tároló csomag Store(n); cout << "\na számok és az előfordulásuk:\n"; Write(); return 0; Az s tároló egy érték-darabszám párokat tartalmazó sorozat. A párok típusa egy rekord (Pair), a sorozatot pedig vector<pair>-ként definiáljuk. A sorozat a csomag összes műveletére nézve globális, de a főcsomag (main.cpp) számára nem látható objektum, ezért őt a container.cpp-ben adjuk meg static minősítéssel. struct Pair{ int value; int no; 368

369 ; static vector<pair> s; Amennyiben a static kulcsszót nem alkalmaznánk, akkor módunkban állna, hogy a main.cpp-ben egy extern vector<pair> s deklarációt követően közvetlenül hozzáférjünk az s adataihoz. Ezt azonban szeretnénk elkerülni, és csak a Store() és a Write() műveleteken keresztül akarjuk használni az s sorozatot. A Store() megvalósítása a programterv s:=s e alprogramja alapján készül. Vegyük észre, hogy az eljárás feje nem utal arra, hogy ez az alprogram az s sorozatot be és kimenő adatként egyaránt használja. Ez az egyik oka annak, hogy a most bemutatott implementációs technikát nem tartjuk kielégítőnek, helyette inkább objektum orientált nyelvi eszközt, az osztályt használjuk szívesebben. void Store(int e) { bool l = false; int ind; for(int i=0;!l && i<(int)s.size(); ++i){ l = s[i].value == e; ind = i; if(l) ++s[ind].no; else{ Pair p; p.value = e; p.no = 1; s.push_back(p); 369

370 A Write() művelet a tároló elemeit kiíró eljárás. void Write() { for(int i=0; i<(int)s.size(); ++i){ cout << s[i].value << " " << s[i].no << endl; Tesztelés Fekete doboz tesztesetek: (Az egyes programozási tételek tesztjét összevonjuk a tételekben szereplő intervallum tesztjével.) 1. A main()-beli összegzés tesztje: a. Egyetlen számot sem adunk meg. b. Több számot adunk meg. 2. A Store()-beli lineáris keresés tesztje: a. Egymás után két különböző számot adunk meg. b. Egymás után két vagy több azonos számot adunk meg. c. Nem közvetlenül egymás után adunk meg két azonos számot. Modulonkénti tesztelés: Store()- Az előző tesztesetek lefedik a modul tesztelését (első elem elhelyezése, a tároló tömbben még nem szereplő elem elhelyezése, létező 370

371 elem ismételt elhelyezése a tároló tömb elején illetve végén), ugyanakkor a modul nem kezeli a memória elfogyásnál bekövetkező eseményt. Write()- Az eddigi tesztesetek lefedik ennek a modulnak a tesztelését. Teljes program main.cpp: #include <iostream> #include <fstream> #include <vector> #include "container.h" using namespace std; int main() { cout << "Adjon meg egész számokat!\n << "Betűvel jelezze a bevitel végét!\n"; int n; while(cin >> n){ Store(n); cout << "\na számok és az előfordulásuk:\n"; Write(); 371

372 return 0; 372

373 container.h: #ifndef CONTAINER_H #define CONTAINER_H #include <vector> void Store(int e); void Write(); #endif container.cpp: #include "container.h" #include <iostream> using namespace std; struct Pair{ int value; int no; ; static vector<pair> s; 373

374 void Store(int e) { bool l = false; int ind; for(int i=0;!l && i<(int)s.size(); ++i){ l = s[i].value == e; ind = i; if(l) ++s[ind].no; else{ Pair p; p.value = e; p.no = 1; s.push_back(p); void Write() { for(int i=0; i<(int)s.size(); ++i){ cout << s[i].value << " " << s[i].no << endl; C++ kislexikon 374

375 állomány-őrszem #ifndef NEV #define NEV #endif csomagra korlátozott globális változó fejállomány forrásállomány static int x; exportált típusok, függvények deklarációi, esetleg változók a fejállományban deklarált elemek definíciói, valamint további segéd elemek (típusok, függvények, esetleg változók) definíciói. 375

376 10. Rekurzív programok kódolása Ez a fejezet egy kakukktojás. Könyvünk első kötetében, a tervezésnél, ugyanis nem eset szó a rekurzív programokról, ezért rekurzív programok kódolásáról sem beszélhetnénk. (Ne tévesszen meg bennünket az, hogy az első kötetben sokat foglalkoztunk az intervallumon értelmezett rekurzív függvényekkel leírt feladatok megoldásáról, amelyeket azonban nem rekurzív programokkal oldottuk meg.) Másfelől, ha egy programban lehetőség van alprogramok hívására, akkor előbb utóbb felvetődik a kérdés, hogy meghívhatja-e egy alprogram önmagát, azaz lehet-e rekurzív alprogram-hívást alkalmazni. Ha ezt megengedjük, akkor rekurzív alprogramról (rekurzív módon hívható, rekurzívan definiált alprogramról) beszélünk. A rekurzív program pedig egy rekurzív alprogramokat tartalmazó program. Számos olyan feladat van, amelynek érthetőbb, jobban áttekinthetőbb megoldása adható meg egy rekurzív programmal. A rekurzív programok tervezése azonban nem egyszerűbb a hagyományos programoknál. Nem kívánunk itt részleteiben foglalkozni a rekurzív programok helyességének és tervezésének kérdéseivel, de gondoljunk csak a tervezés azon sarkalatos pontjára, amely keretében a program leállását vizsgáljuk. Eddig csak a ciklusok okozhattak az operációs rendszer által fel nem ismerhető végtelen működést, rekurzív programoknál garanciát kell tudnunk mutatni arra, hogy egy alprogram legfeljebb véges sokszor hívhatja meg közvetve vagy közvetlenül önmagát. Ebben a fejezetben néhány C++ nyelven kódolt rekurzív programot mutatunk be, mivel ezek az eddig látott nyelvi elemeken kívül mást nem igényelnek. A konkrét feladatoknál kitérünk majd az felhasznált algoritmusok helyességének vizsgálatára is, anélkül, hogy általánosságban foglakoznánk helyesség kérdésével. Implementációs stratégia Mindig a tervezési fázisban dől el az, hogy egy feladat megoldására rekurziót alkalmazunk-e, illetve hogy azt rekurzív programmal kódoljuk-e. Itt rögtön különbséget kell tudnunk tenni három fogalom 376

377 között. Egy feladat leírása, specifikálása is történhet rekurzív függvények segítségével (erre láthattunk példát az első kötetben), adhatunk egy feladatra rekurzív algoritmus képében absztrakt megoldást, és kódolhatjuk azt annak szerkezetéhez hűen ragaszkodva rekurzív formában, de át is alakíthatjuk azt nem-rekurzív algoritmussá. Például egy természetes szám faktoriálisát kiszámíthatjuk összeszorzásokkal az összegzés programozási tétele alapján (lásd 6. fejezet), ahol sem a megoldás ötlete, sem a megoldó algoritmus, sem annak kódja nem tartalmaz rekurziót. Támaszkodhatunk azonban az ismert n! = n*(n 1)! (és 1! = 1) rekurzív képletre is, amelyre alkalmazhatjuk a rekurzív függvény értékét kiszámító, azaz nemrekurzív programot eredményező programozási tételt (lásd első kötet), de tervezhetünk egy rekurzív alprogramot is, amelyen a specifikációban szereplő rekurzív képlet tükröződik. f := Fact(n) n>1 f := n*fact(n-1) f := 1 Azt, hogy ez az alprogram a rekurzív képletnek felel meg, nem kell bizonygatni. Ezért, amennyiben a képlet helyes, az alprogramot meghívó program az alprogramtól a kívánt eredmény kapja majd vissza. De vajon biztosan leáll-e ez az alprogram? Egy rekurzív alprogram leállásának szükséges feltétele az, hogy legyen olyan végrehajtása is, amely nem hívja meg újra önmagát. Ehhez legalább egy elágazásra vagy egy ciklusra van szükség az alprogramban. A fenti példában az elágazás jobboldali ága nem tartalmaz rekurzív hívást. A leállás másik feltétele, hogy ne fordulhasson az elő, hogy az alprogram mindig újabb rekurzív hívást hajtson végre, azaz véges számú rekurzív 377

378 hívása után bizonyosan a rekurziómentes ágra fusson rá a vezérlés. A fenti alprogram minden egyes hívásával eggyel kisebb értéket ad az n paraméterváltozónak. Ez a garancia arra, hogy véges számú hívás után az 1 értékkel hívódik meg az alprogram, amikor is az már nem fog újabb rekurzív hívást kezdeményezni. Egy rekurzív alprogram lokális változójának annyi példánya lesz, ahányszor az alprogramot meghívják. A fenti példában nincs értelme feltenni azt a kérdést, hogy mennyi az n változó értéke, mert nem beszélhetünk egyetlen n változóról. A vezérlés szempontjából azonban mindig pontosan megállapítható az alprogram legutoljára meghívott és még be nem fejezett változatának változói, és ezek értéke egyértelműen meghatározható. Minden rekurzív program átalakítható nem-rekurzív programmá (sőt fordítva is). Az átalakítást többnyire a hatékonyság javításának céljából tesszük meg, bár ilyenkor azt is mérlegelni kell, hogy az így nyert program átláthatósága, és ennek következtében az érthetősége, javíthatósága mennyire romlik a rekurzív változathoz képest. Egy rekurzív program nagy előnye ugyanis a tömör, áttekinthető leírás. Vannak olyan programozási nyelvek is, amelyek nem támogatják a rekurzív alprogramok készítését, és ilyenkor a nem-rekurzív átalakítás az egyetlen járható út. Nyelvi elemek A rekurzív alprogramok kódolásának nyelvi eszközei ugyanazok, mint amelyeket a közönséges alprogramok kódolásánál megismertünk. Fel kell azonban hívni még egyszer arra a figyelmet, hogy egy alprogram minden hívása után a lokális változói újból létrejönnek és az adott hívás befejeződéséig a verem memóriában maradnak. Az ismétlődő rekurzív hívások ugyanazon lokális változók újabb és újabb példányait hozzák létre a verem memóriában. Az új lokális változóknak a név- és típusegyezésen kívül semmi kapcsolatuk sincs a korábbi meghívásakor létrejött lokális változókkal. A verem memóriában egyidejűleg léteznek az alprogram korábbi és újabb 378

379 hívása során létrejön lokális változók. Az újbóli hívás számára a korábbi hívás azonos nevű lokális változói nem érhetőek el, azok értékét ha szükség lenne rájuk paraméterátadással kell továbbadni. Az ismétlődő rekurzív hívásokat tartalmazó program működésének egyik veszélye, hogy a rendelkezésre álló memória gyorsan elfogyhat. Ezért gondosan meg kell válogatni, hogy melyek legyenek egy rekurzív alprogram lokális változói. A nagy méretű adatokat tartalmazó változókat célszerű hivatkozás szerint átadni, vagy azt megfontolni, hogy egy-egy ilyen változó kiemelhető-e globális változóvá a rekurzív alprogramból. 379

380 24. Feladat: Binomiális együttható Számoljuk ki a k-adik n-ed rendű binomiális együtthatót, az -t! Specifikáció A feladat specifikációja igen egyszerű. A = ( n,k,b : N ) Ef = ( n=n k=k k [0..n] ) Uf = ( n=n k=k b = ) A binomiális együtthatókat faktoriálisok segítségével szokták definiálni: n N: k [0..n]: Egy ehhez nagyon közel álló másik képlet a Nekünk most egyik sem felel meg, hiszen ezek nem rekurzív képletek. (Pedig ennek kódolása is tanulságos, mert itt ügyelni kell arra, hogy az i-vel az n k 1-ről induljunk egyesével csökkenően, és minden lépésben először a számlálóval szorozzunk, utána a nevezővel osszunk, hogy a részeredmények mindig egész számok maradjanak. ) A feladat azonban megfogalmazható rekurzív képlet segítségével is. Az egyik ilyen képlet a binomiális együttható eredeti definíciójából származtatható: n N: és n 2, k [1..n 1]: 380

381 A másik definíció az úgynevezett Pascal háromszög törvényszerűségét használja ki: n N: és n 2, k [1..n 1]: Absztrakt program Mindkét rekurzív képlethez elkészítjük az absztrakt rekurzív programot, de végül az elsőt fogjuk implementálni. Mindkét rekurzív program helyettesíthető egy nem-rekurzív változattal, a második megoldás egy igen egyszerűvel, de mivel ebben a fejezetben a rekurzív hívású alprogramokkal foglalkozunk, ezeket a változatokat nem mutatjuk be. Az első rekurzív képletnek megfelelő absztrakt algoritmus legfeljebb n- szer hívja meg önmagát. b := Binomial(n,k) k = 0 b := 1 b := Binomial(n,k 1)* (n k+1)/k Ennek megvalósításnál ügyelni kell arra, hogy először az n k+1-val történő szorzást végezzük el és utána a k-val való osztást, hogy ne keletkezzen részeredményként törtszám, mert a törtrészek elvesztésével sérülne a végeredmény. Sajnos emiatt előfordulhat, hogy amikor az eredmény még elférne ugyan az eredmény változónak a megvalósításnál kijelölt típusa által meghatározott memória területén, a számolás részeredménye már túlcsordul azon. Ezért az implementációhoz a második változatot használjuk fel. b := Binomial(n,k) n = 0 k = 0 k = n 381

382 b := 1 b := Binomial(n 1,k 1)+Binomial(n 1,k) Igaz, hogy ennek idő hatékonysága rosszabb, az kiszámolásához akár 2 n - szer is meghívódik az alprogram, ráadásul többször ugyanazon paraméterekkel, de cserébe nem igényel sem szorzást, sem osztást, és a részeredmény túlcsordulása sem következhet be. Implementálás Komponens szerkezet A binomiális együttható kiszámolását a main függvényben találjuk. Itt felhasználjuk a korábban már bemutatott ReadInt() függvényt a bemenő adatok beolvasásához. Ezt a függvényt külön csomagban (read.hread.cpp) csatoljuk az alkalmazásunkhoz. A main függvény legfontosabb része a Binomial() rekurzív függvény hívása. Ezt a függvényt a main függvénnyel együtt a main.cpp állományban helyezzük el. main.cpp main() Binomial() read.h - read.cpp ReadInt() Nat() Függvények hívási szerkezete ábra. Komponens szerkezet A vezérlést, azaz a komponensek egyes függvényeinek megfelelő sorrendben történő meghívását a main függvény biztosítja. ReadInt() main() Nat() Binomial() 382

383 10-2. ábra. Alprogramok hívási lánca 383

384 A fő program A binomiális együttható kiszámolását ciklikusan ismételhető módon valósítjuk meg a main függvényben. int main() { char ch; do{ int n = ReadInt("n= ", "Természetes szám kell!\n",nat); int k = ReadInt("k= ", "Természetes szám kell!\n",nat); cout << "B(n,k)= " << Binomial(n,k) << endl; cout << "Folytatja? (I/N): "; cin >> ch; while(ch!= 'n' && ch!= 'N'); return 0; Rekurzív függvény A Binomial() függvényt a terv alapján kódoljuk. int Binomial(int n, int k) { 384

385 if(0 == n 0 == k k == n) return 1; else return Binomial(n-1,k-1)+Binomial(n-1,k); 385

386 Tesztelés Fekete doboz tesztesetek: 1. =1 2. =1 3. =1 4. =1 5. =1 6. Általános esetek. 7. Skálázás. (Annak kimérése, hogy legfeljebb mekkora n és k értékekre tudjuk kiszámolni az eredményt, és ez mennyire tart sokáig.) Modul tesztek: main()- A ciklikusan ismételhetőség kipróbálása. ReadInt(),Nat()- Ezeket a függvényeket korábban már többször használtuk, a tesztelésük ott megtalálható. Binomial()- A fekete doboz tesztesetek lefedik a tesztelést. 386

387 Teljes program main.cpp: #include <iostream> #include "read.h" using namespace std; int Binomial(int n, int k); int main() { char ch; do{ int n = ReadInt("n= ", "Természetes számot kérek!\n",nat); int k = ReadInt("k= ", "Természetes számot kérek!\n",nat); cout << "B(n,k)= " << Binomial(n,k) << endl; cout << "Folytatja? (I/N): "; cin >> ch; while(ch!= 'n' && ch!= 'n'); return 0; 387

388 int Binomial(int n, int k) { if(0 == n 0 == k k == n) return 1; else return Binomial(n-1,k-1)+Binomial(n-1,k); read.h: #ifndef READ_H #define READ_H #include <string> bool ci(int k); bool Nat(int n); int ReadInt (std::string msg, std::string errormsg, bool cond(int) = ci); #endif read.cpp: #include "read.h" #include <iostream> 388

389 using namespace std; bool ci(int k){ return true; bool Nat(int n) { return n >= 0; int ReadInt(string msg, string errormsg, bool cond(int) ) { int n; int hiba = true; string tmp; do{ cout << msg; cin >> n; if(cin.fail()!check(n)){ cout << errmsg << endl; cin.clear(); getline(cin,tmp); while(hiba); return n; 389

390 25. Feladat: Hanoi tornyai Adjunk megoldást a Hanoi tornyai problémára! Ebben a játékban több különböző méretű lyukas korong helyezkedik el három rúd valamelyikén. Egyszerre mindig csak egy korong rakható át egy másik rúdra, de úgy, hogy soha nem tehetünk nagyobb korongot kisebb korong tetejére. Kezdetben minden korong az 1-es sorszámú rúdon található. Milyen mozgatásokkal vihető át az összes korong a 3-as rúdra? Specifikáció Egy korong mozgatása két rúd-sorszámmal (honnan-hova) adható meg. Egy ilyen mozgatást leíró (i,j) számpárt a továbbiakban a szemléletesség kedvéért (i j) alakban fogjuk írni. A cél a megfelelő mozgatás-sorozat előállítása. A = ( n : N, ss : ( N N ) * ) Ef = ( n=n ) Uf = ( n=n ss = Hanoi(n,1,3,2) ) Bevezetjük a Hanoi(n,i,j,k) szimbólumot annak a mozgatás-sorozatnak a jelölésére, amelyik n darab korongot az i. rúdról a j. rúdra a k. rúd segítségével visz át. Ennek jelentése egy rekurzív képlettel adható meg: Hanoi(1,i,j,k) = <(i j)> Hanoi(n,i,j,k) = Hanoi(n 1,i,j,k) < (i j) > Hanoi(n 1,k,j,i) (ha n>1) Absztrakt program A megoldás egy rekurzívan hívható alprogram lesz. ss := Hanoi(n,i,j,k) n = 1 ss := <(i j)> ss := Hanoi(n 1,i,j,k) <(i j)> Hanoi(n 1,k,j,i) 390

391 Implementálás A megoldó programot közvetlenül, rekurzívan hívható függvény segítségével kódoljuk. ReadInt() main() Nat() Hanoi() ábra. Alprogramok hívási láncai Az alkalmazás szerkezete szinte szó szerint megegyezik az előző feladat megoldáséval. main.cpp main() Hanoi() read.h - read.cpp ReadInt() Nat() ábra. Komponens szerkezet A fő program A probléma megoldását ciklikusan ismételhető módon valósítjuk meg a main függvényben. int main() { char ch; do{ int n = ReadInt("n= ", "Természetes számot kérek!\n",nat); 391

392 cout << Hanoi(n,1,3,2) << endl; cout << "Folytatja? (I/N): "; cin >> ch; while(ch!= 'n' && ch!= 'N'); return 0; Rekurzív függvény A Hanoi() függvényt a terv alapján kódoljuk. Az eredmény sztringet egy ostringstream típusú objektumban állítjuk össze. Találkoztunk már ezzel a cout-hoz hasonló adatfolyammal, amelybe betett adatok egy sztringbe fűződnek fel, és ezt a sztringet tudjuk aztán az str() függvénnyel lekérdezni. string Hanoi(int n, int i, int j, int k) { ostringstream ss; if(1 == n) ss << i << "->" << j; else ss << Hanoi(n-1,i,k,j) << ", " << i << "->" << j << ", " << Hanoi(n-1,k,j,i); return ss.str(); Tesztelés Fekete doboz tesztesetek: 392

393 1. Hanoi(1,1,3,2) 2. Hanoi(2,1,3,2) 3. Hanoi(3,1,3,2) 4. Általános eset. 5. Skálázás. (Annak kimérése, hogy legfeljebb mekkora n értékekre tudjuk kiszámolni az eredményt, és ez mennyire tart sokáig.) Modul tesztek: main()- A ciklikusan ismételhetőség kipróbálása. ReadInt(),Nat()- Ezeket a függvényeket korábban már többször használtuk, a tesztelésük ott megtalálható. Hanoi()- A fekete doboz tesztesetek lefedik a tesztelést. 393

394 Teljes program main.cpp: #include <iostream> #include <sstream> #include "read.h" using namespace std; string Hanoi(int n, int i, int j, int k); int main() { char ch; do{ int n = ReadInt("n= ", "Természetes számot kérek!\n",nat); cout << Hanoi(n,1,3,2) << endl; cout << "Folytatja? (I/N): "; cin >> ch; while(ch!= 'n' && ch!= 'N'); return 0; string Hanoi(int n, int i, int j, int k) { 394

395 ostringstream ss; if(1 == n) ss << i << "->" << j; else ss << Hanoi(n-1,i,k,j) << ", " << i << "->" << j << ", " << Hanoi(n-1,k,j,i); return ss.str(); read.h: #ifndef READ_H #define READ_H #include <string> bool ci(int k); bool Nat(int n); int ReadInt(std::string msg, std::string errmsg, bool cond(int)= ci); #endif read.cpp: #include "read.h" #include <iostream> using namespace std; 395

396 bool ci(int k){ return true; bool Nat(int n) { return n >= 0; int ReadInt(string msg, string errmsg, bool cond(int) ) { int n; int hiba = true; string tmp; do{ cout << msg; cin >> n; if(cin.fail()!cond(n)){ cout << errmsg << endl; cin.clear(); getline(cin,tmp); while(hiba); return n; 396

397 26. Feladat: Quick sort Implementáljuk a gyorsrendezést (quick sort)! Specifikáció A = ( v : Z k ) Ef = ( v=v ) Uf = ( v=rendezett(v ) ) Absztrakt program Gyors rendezésnek (Quick sort) nevezik azt a módszert, amelyik a rendezendő tömböt felosztja három szakaszra úgy, hogy a középső egyetlen elemből álljon, továbbá az elemek cserélgetésével eléri azt, hogy első szakasz minden eleme kisebb vagy egyenlő, a harmadik szakasz elemei pedig nagyobb vagy egyenlők legyenek a középső elemnél. Ezt a felosztást kell rekurzívan megismételni az első és a harmadik szakaszra mindaddig, amíg csupa egy hosszúságú, önmagában tehát rendezett szakaszokat kapunk. Ekkor a tömb már növekvően rendezett lesz. A felosztást végző algoritmust általánosan, a v tömb m-től n-ig indexelt szakaszára fogalmazzuk meg. Az algoritmusnak több változata is ismert, itt az egyiket mutatjuk be. Az algoritmus eredményeképpen átrendeződik majd a tömb m-edik pozíciójától n-edik pozíciójáig terjedő szakasza. A kezdetben az n-edik pozíción található elem (ennek értékét az x lokális változó tartalmazza majd) az m és n közé eső a-adik pozícióra kerül úgy, hogy ez legyen a középső elem. Eszerint az a-nál kisebb pozíciójú helyekre a tömb m és n közé eső elemei közül a kisebb-egyenlők, az a-nál nagyobb pozíciójú helyeke az m és n közé eső számok közül a nagyobb-egyenlő értékek kerülnek. Az algoritmus bevezet még egy f segédváltozót is. A külső ciklusnak invariánsa szerint az a- dik elem előtti tömbbeli értékek kisebbek, az f-dik elem utáni értékek pedig nagyobbak az x értéknél. 397

398 a:= Divide(v,m,n) x, a, f := v[n], m, n a<f v[a] x a<f a := a+1 a<f v[f], f := v[a], f-1 SKIP v[f] x a<f f := f-1 a<f v[a], a := v[f], a+1 SKIP v[a] := x Ezt a Divide() függvényt hívja a gyorsrendezés rekurzív programja. Quick(v,m,n) m = n SKIP a := Divide(v,m,n) Quick(v,m,a-1) Quick(v,a+1,n) Szokás az elágazás feltételét úgy enyhíteni, hogy ha m és n eltérése már elég kicsi, akkor egy másik fajta (pl. beillesztéses) rendezéssel rendezzük a tömb kijelölt szakaszát. Ezáltal a rendezés még gyorsabb lesz. Implementálás 398

399 A megoldó programot közvetlenül, rekurzívan hívható függvény segítségével kódoljuk. Komponens szerkezet A rendezés algoritmusa a main.cpp állományban kerül kódolásra. Fontos alprogramja a Quick() rekurzív eljárás, és az ebben hívott Divide() függvény. A tömb szöveges állományból való feltöltését (Read() és a tömb konzolablakba történő kiírását (Write()) külön csomagban (array.harray.cpp) helyezzük el. main.cpp main() Quick() Divide() array.h - array.cpp Read() Write() ábra. Komponens szerkezet Függvények hívási szerkezete A vezérlést, azaz a komponensek egyes függvényeinek megfelelő sorrendben történő meghívását a main függvény biztosítja. Read() main() Write() Quick() Divide() ábra. Alprogramok hívási láncai A fő program 399

400 A main függvény hozza létre a rendezendő tömböt, kiírja azt a konzolablakba, meghívja rá a gyorsrendezést, végül kiírja a rendezett alakot. 400

401 int main() { vector<int> v; Read(v); cout << "Rendezés előtt: \n"; Write(v); Quick(v,0,(int)v.size()-1); cout << "Rendezés után: \n"; Write(v); return 0; Rekurzív eljárás A Quick() eljárást, és a Divide() függvényt a terv alapján kódoljuk. void Quick(vector<int> &v, int m, int n) { if( m < n ){ int a = Divide(v,m,n); Quick(v,m,a-1); 401

402 Quick(v,a+1,n); int Divide(vector<int> &v, int a, int f) { int x = v[f]; while(a<f){ while( a<f && v[a]<=x ) ++a; if( a<f ){ v[f] = v[a]; --f; while( a<f && v[f]>=x ) --f; if( a<f ){ v[a] = v[f]; --a; v[a] = x; return a; 402

403 Tömb műveletek Egy egydimenziós tömbbel kapcsolatban két műveletet vezetünk be. Az egyik művelet egy szöveges állományból tölt fel egy tömböt. Az állomány első adata a tömb elemszámát, az azt követő adatai pedig a tömb elemeit tartalmazza. Feltesszük, hogy ezek mind egész számok, és a tömb hosszát megadó szám sem negatív. void Read(vector<int> &t) { ifstream f("input.txt"); if (f.fail()) { cout << "Hibás fájlnév!\n"; exit(1); ; int n; f >> n; t.resize(n); for (int i=0;i<n;++i){ f >> t[i]; A másik művelet egy tömb elemeit a konzolablakba írja ki. 403

404 void Write(const vector<int> &t) { for(int i=0; i<(int)t.size(); ++i) { cout << "\t" << t[i]; cout << endl; Tesztelés Most csak a Quick() és a Divide() teszteléséhez elegendő fekete doboz teszteseteket adjuk meg. A tesztelés többi részét az Olvasóra bízzuk. 1. Nulla hosszúságú tömb rendezése. 2. Egy elemű tömb rendezése. 3. Kettő-hatvány darab elemet tartalmazó tömb rendezése. 4. Kettő-hatványtól eltérő elemszámú tömb rendezése. 5. Csupa eltérő elemű tömb rendezése. 6. Csupa azonos elemű tömb rendezése. 7. Több azonos elemet is tartalmazó tömb rendezése. 8. Rendezett tömb rendezése. 404

405 Teljes program main.cpp: #include <iostream> #include "array.h" using namespace std; void Quick(vector<int> &v, int m, int n); int Divide(vector<int> &v, int m, int n); int main() { vector<int> v; Read(v); cout << "Rendezés előtt: \n"; Write(v); Quick(v,0,(int)v.size()-1); cout << "Rendezés után: \n"; Write(v); return 0; 405

406 void Quick(vector<int> &v, int m, int n) { if( m < n ) { int a = Divide(v,m,n); Quick(v,m,a-1); Quick(v,a+1,n); int Divide(vector<int> &v, int a, int f) { int x = v[f]; while(a<f){ while( a<f && v[a]<=x ) ++a; if( a<f ){ v[f] = v[a]; --f; while( a<f && v[f]>=x ) --f; if( a<f ){ v[a] = v[f]; --a; v[a] = x; return a; array.h: 406

407 #ifndef ARRAY_H #define ARRAY_H #include <vector> void Read(std::vector<int> &t); void Write(const std::vector<int> &t); #endif array.cpp: #include "array.h" #include <fstream> #include <iostream> using namespace std; void Read(vector<int> &t) { ifstream f("input.txt"); if (f.fail()) { cout << "Hibás fájlnév!\n"; exit(1); ; 407

408 int n; f >> n; t.resize(n); for (int i=0;i<n;++i){ f >> t[i]; void Write(const vector<int> &t) { for(int i=0; i<(int)t.size(); ++i){ cout << "\t" << t[i]; cout << endl; 408

409 III. RÉSZ PROGRAMOZÁS OSZTÁLYOKKAL Az összetettebb feladatok megoldásához gyakran kell bevezetnünk olyan adattípusokat, amelyek nem szerepelnek a választott programozási nyelv típusai között. Az ilyen úgynevezett felhasználói típusok értékeinek számítógépes ábrázolásáról, azaz reprezentálásáról, valamint a típus műveleteinek implementálásáról ilyenkor magunknak kell gondoskodni. A nevezetes szerkezetű típusok reprezentálásához a magas szintű programozási nyelvek jól használható nyelvi elemeket biztosítanak. Például C++ nyelven a rekord szerkezetű típusok a struct szerkezettel írhatóak le, és az ilyen típusú változók (objektumok) komponenseire a mező nevek (szelektorok) segítségével hivatkozhatunk. A típusműveleteket tehát ebben az esetben a programozási nyelv szolgáltatja. Ugyanez a helyzet a tömbökkel is. Jól használható C++ nyelvben az enum szerkezet az olyan típus értékeinek felsorolására (értelemszerűen csak véges sok típusértékről lehet szó), amely az értékek összehasonlításain kívül nem rendelkezik más típusművelettel. Az alternatív szerkezetű típusokkal mostohán bánnak a programozási nyelvek. A C++ nyelvbeli union szerkezet is csak első látásra tűnik alkalmas eszköznek az alternatív szerkezetű típusok definiálására, de nem rendelkezik olyan művelettel, amely egy ilyen típusú változóban éppen tárolt érték típusát megmutatná. A felhasználói típus definiálásának legelegánsabb eszköze az osztály (class). Ezt akkor használjuk, ha az adattípus olyan műveletekkel rendelkezik, amelyet nem biztosítanak közvetlenül a választott programozási nyelv egyéb eszközei (és persze rendelkezik az adott nyelv az osztály definíció lehetőségével, vagy ahhoz hasonló nyelvi elemmel). Egy osztály segítségével tetszőleges szerkezetű típusértékek (objektumok) ábrázolhatók azáltal, hogy egy típusérték komponenseinek értékei számára változókat (adattagok) definiálhatunk, amelyekhez műveletek (metódusok) készíthetők. Ebben a részben olyan feladatokkal foglalkozunk, ahol egy vagy több olyan felhasználói típust kell a megoldáshoz megvalósítani, amelyek leírásához osztályt használnunk. Az ilyen megoldásokban a tervezésben és

410 a megvalósításban egyaránt egy sajátos adattípus központú, típus-orientált szemlélet uralkodik. Ahogy a hátra levő fejezetekben egy-egy feladat megoldásában egyre több osztály kerül bevezetésre és ezek egymáshoz való viszonya is egyre érdekesebbé válik, úgy jelennek meg fokozatosan programjainkban az objektum-orientált programozási stílus jegyei is. Nincs azonban arról szó, hogy a korábbi módszerek, tapasztalatok feleslegessé válnának, hiszen például egy-egy típusművelet megvalósítása az eddigi (hagyományos, procedurális) szemlélet mentén történik. A két programozási paradigma egymást erősítve, kiegészítve jelenik meg, és a megoldandó feladat sajátosságán múlik, hogy melyik dominál a megoldó programban. Az illusztrációként választott feladatok megoldásának megtervezésekor az első kötet típus központú szemléletére támaszkodó módszereket használjuk, mert az kiváló alapot ad az objektum-orientált programozási stílus bevezetéséhez és természetes módon köti össze azt a procedurális programozási stílussal. Újdonsága miatt a következő néhány fejezetben nyilvánvalóan az objektumorientált programozási stílusra koncentrálunk, ennek implementációs stratégiáit és nyelvi eszközeit mutatjuk be. A fenti paradigmaváltás maga után vonja a tesztelésnél alkalmazott stratégiák módosítását is. A modul tesztek innentől kezdve nemcsak egy-egy alprogram önálló tesztelését jelentik, hanem egy-egy osztályét is. Az osztályok mint azt látni fogjuk a komponensek tulajdonságjegyeivel rendelkeznek, ezért önálló és teljes körű tesztelésük csak külön tesztkörnyezet segítségével végezhető el. Ennek keretében kell az egyes típusműveletek célját, hatását tesztelni. A fekete doboz tesztesetek a típusműveletek különféle variációinak kipróbálásából állnak. Természetesen a megoldásban résztvevő osztályok tesztelése után most sem maradhat el a fő feladat fekete doboz teszteseteinek kipróbálása sem. 410

411 11. A típus megvalósítás eszköze: az osztály Programjaink tervezésekor két féle adattípussal találkozhatunk. Olyanokkal, amelyeknek van a választott programozási nyelvben megfelelője, illetve olyanokkal, amelyeknek nincs. Ez utóbbiakat magunknak kell definiálni. Az objektum-orientált programozási nyelvek ehhez a definícióhoz hathatós segítséget nyújtanak egy különleges nyelvi elem formájában, amelyet osztálynak nevezünk. Implementációs stratégia Tekintsünk el egyelőre attól, hogy egy objektum-orientált programozási nyelvben az osztály milyen sokrétű, kifinomult nyelvi lehetőségeket biztosít a programozó számára, és helyette vezessünk be egy végletekig leegyszerűsített osztály fogalmat. Az osztályra olyan egységként gondoljunk, amely változókat és alprogramokat tartalmaz, ahol az alprogramokkal a változók értékeit kérdezhetjük le vagy változtathatjuk meg, illetve a változók értékétől függő tevékenységet hajthatunk végre. Az osztálynak van egy neve, amellyel azonosítani tudjuk. Egy osztály példányosítása 1 azt jelenti, hogy az osztály leírása alapján speciális memória-foglalást hozunk létre: ez az objektum. Egy objektum számára lefoglalt területen azok a változók foglalnak helyet, amelyeket az osztályban vezettünk be. Ezeket adattagoknak (tagváltozóknak) hívjuk. Valahányszor újabb és újabb objektumát (példányát) hozzuk létre az osztálynak, mindannyiszor az osztály változóinak, azaz adattagjainak újabb és újabb példányai jönnek létre. Egy-egy objektumra annak deklarálásakor kiválasztott nevével tudunk majd hivatkozni, ugyanakkor az objektum adattagjaira a közvetlen hivatkozást általában nem engedjük meg, őket 1 A szakirodalom ezt a kifejezést nemcsak erre, hanem egy a későbbiekben tárgyalt másik fogalomra (sablon példányosítás) is bevezette, ezért erre az esetre mi inkább az objektum létrehozása kifejezést fogjuk használni a továbbiakban. 411

412 kizárólag az osztály leírásán belül, az osztály alprogramjaiban használhatjuk. A metódus (tagfüggvény) egy osztályon belül definiált alprogram, amelyet az osztály egy objektumával kapcsolatban lehet meghívni, és ezzel az objektummal, illetve az objektum adattagjaival kapcsolatos tevékenységet hajt végre. Az osztály tökéletesen alkalmas eszköz arra, hogy nyelvi szinten leírjon egy adattípust. Egy adat típusán az adat által felvehető típus-értékek halmazát, az azokra megfogalmazott típus-műveleteket, típus-értékeinek reprezentációját és a típus-műveleteket implementáló programokat értjük. Ha ismert egy adattípus leírása, akkor ahhoz könnyen elkészíthető az azt megvalósító osztály. típus-értékek: A típus által definiált értékek. típus-műveletek: A típus-értékekkel végzendő tevékenységek. típus-reprezentáció: Egy típus-érték ábrázolására szolgáló érték-együttes leírása. típus-invariáns: Egy típus-értéket helyettesítő érték-együttesre előírt feltétel. típus-implementáció: A típus-műveletek tevékenységét elvégző program, amely a típus-érték helyett annak reprezentációjával dolgozik ábra. Az adattípus leírásának részei Az osztály neve a megvalósítandó típus neve lesz, adattagjai a típus reprezentációjában bevezetett érték-együttes tagjait tartalmazó változók, metódusai a típus-műveletek, azok törzsei pedig a típus-implementáció programjai. Egy objektum egy típusérték tárolására szolgál. Az osztályban deklarált adattagok a típus reprezentációjának leírására szolgálnak. Ha egy típusértéket különféle adatokkal reprezentálunk, akkor ezek az adatok egy-egy ilyen adattagban kerülnek elhelyezésre. Az 412

413 adattagokra sokszor fogalmazunk meg olyan megszorításokat, amelyek együttesen a típus invariánsát teljesítik. Az osztály egy metódusa felel meg a típus egy műveletének. A metódusokat mindig az osztály egy objektumára hívjuk meg, azaz a paraméterei között mindig szerepel legalább egy objektum. Tulajdonképpen a metódusok egy objektummal kapcsolatos tevékenységeknek tekinthetők. A helyesen megtervezett típus-műveletek kihasználják és megőrzik a művelet által használt objektumok tagváltozóira vonatkozó típus-invariánst. Más szavakkal, ha van egy olyan objektumunk, amely adattagjai kielégítik a típusinvariánst, és a művelet megváltoztatja az objektum adattagjait, akkor azok új értékei is megfelelnek majd a típus-invariáns követelményeinek. típus típus-értékek típusreprezentáció típus-műveletek típus-műveletek implementációi osztály class típus { private: adattagok public: konstruktor ábra. Adattípust megvalósító osztály Egy típus osztályként való újrafogalmazáskor arra is kell ügyelni, hogy a típus-invariáns már egy objektum létrehozásakor is fennálljon az objektum adattagjaira. Erre az osztálynak egy speciális metódusa szolgál: a konstruktor. Ez a metódus akkor hívódik meg, amikor létrehozunk egy új objektumot, és ennek a metódusnak a törzse gondoskodhat a születő objektum adattagjainak megfelelő, típus-invariáns szerinti kezdeti értékadásáról. 413

414 Az osztályokat tartalmazó programok tesztelése is újabb módszereket kíván. Egy-egy osztály önálló komponensként jelenik meg egy alkalmazásban, ezért azt önmagában is tesztelni kell. teszteljük az eddig látott módon az egyes metódusok működését, de tesztelni kell a metódusok tetszőleges variációinak hatását is. Egy komponens teszteléséhez érdemes külön tesztkörnyezetet, egy menüt biztosító keretprogramot készíteni, mert többnyire az a főprogram, amelyen keresztül igénybe vesszük egy komponens szolgáltatásait, nem képes teljes körűen letesztelni azt. Nyelvi háttér Az előző pontban bevezetett osztály tulajdonképpen nem nyelvi, hanem tervezési eszköz, a típus szinonimája, amelynek azonban a nyelvi szinten történő megfogalmazása sem bonyolult. Kell egy kulcsszó (mondjuk class), amely bevezeti az osztályt leíró blokkot, ezután megadjuk az osztály egyedi nevét, majd a leírást tartalmazó blokkban felsoroljuk az adattagokat és a metódusokat. Ebben és a következő fejezetben feltételezzük, hogy az adattagok mindegyikéhez automatikusan rendelődik memóriafoglalás (eddig amúgy is csak ilyen változókkal találkoztunk). Ha nem ilyen lenne, akkor az onnan ismerhető fel, hogy az adattagok között úgynevezett pointer-változó jelenik meg, amelyhez explicit módon (new utasítással) kell a saját kódunkban (sokszor a konstruktorban) memóriafoglalást létrehoznunk. A pointer adattagok jelenléte lényeges vízválasztó az osztályok kezelésében. Egyelőre mi az egyszerű osztályokkal foglalkozunk, amelyek nem igényelnek explicit módon végzett dinamikus helyfoglalást. Az alábbiakban egy osztály C++ nyelvi megvalósítását láthatjuk. // Osztály definíciója class Tipus { private: // adattagok Tipus11 tag1; Tipus12 tag2; 414

415 public: // konstruktorok Tipus(); Tipus( ); // destruktor ~Tipus(); // metódusok deklarációi Tipus21 Metod1( ); Tipus22 Metod2( ); ; // Konstruktorok és a destruktor definíciói Tipus::Tipus() { Tipus::Tipus( ) { Tipus::~Tipus( ) { // Metódusok definíciói Tipus21 Tipus::Metod1( ){ Tipus22 Tipus::Metod2( ){ 415

416 Ha adott egy Tipus nevű osztályunk, akkor a segítségével (annak mintájára) létrehozhatunk objektumokat, amelyekre változók segítségével hivatkozhatunk. Az alábbi esetben a t változó azonosít majd egy objektumot. Tipus t Egy osztály sajátosan jelöli ki a benne definiált tagok láthatóságát. Alapértelmezés szerint egy osztály minden tagja privát, azaz rájuk csak az osztályon belül, azaz az osztályt definiáló blokkban és az osztályhoz tartozó metódusok törzsében lehet hivatkozni. Ez utóbbit azért hangsúlyozzuk, mert egy metódus definíciója, azaz a metódus törzse nem feltétlenül az osztály blokkján belül helyezkedik el. (Megjegyezzük, hogy ezzel enyhül a szokásos láthatósági szabály, mely szerint egy azonosító láthatósága arra blokkra terjed ki, amelyikben deklarálták.) A privát tagokat az egyértelműség kedvéért private-ként jelöljük meg akkor is, ha ez a tulajdonság alapértelmezett. Ha azt szeretnék biztosítani, hogy egy tagváltozó vagy különösen egy metódus az osztályon kívül is látható legyen, akkor ezt a public kulcsszó segítségével jelölhetjük ki. A publikus tagok láthatósága kiterjed minden olyan helyre, ahol maga az osztály látható. Egy objektum adattagjait általában privátként szokták deklarálni, de ha mégis publikussá tesszük, akkor a t.tag1 formában lehetne rájuk hivatkozni. Ha viszont az elérhetősége privát marad, akkor közvetlenül nem hivatkozhatunk rá az osztályon kívül, a fenti kifejezésre fordítási hibát kapunk. Ha egy metódust az osztály leíráson kívül akarjuk meghívni az osztály egy objektumára, akkor a metódus láthatósága publikus kell legyen. Egy metódus abban különbözik a szokásos alprogramtól, hogy csak az adott osztály egy objektumával (ez a hívó objektum) lehet meghívni. A hívó objektumnak a metódus neve előtt kell a hívásban szerepelnie: t.metod1( ) 416

417 (A C++ nyelven később látunk majd p->method1( ) alakú hívásokat is, amennyiben a p egy objektumra mutató pointer-változó lesz.) Amikor a metódust meghívják, akkor ez a hívó objektum (illetve annak egy hivatkozása), aktuális paraméterként adódik át egy különleges formális paraméterváltozónak. Ennek a neve többnyire kötött (C-szerű nyelvekben ez a this, más nyelveken esetleg self), ezzel a névvel tudunk a hívó objektumra hivatkozni a metódus törzsében. Sok nyelvben (C++, Java, C#, Object Pascal) a metódus formális paraméterlistája ezt a változót explicit módon nem is tartalmazza, ez egy alapértelmezett paraméterváltozó, de van olyan nyelv is (Python), ahol fel kell tüntetni a formális paraméterlistában. A C++ nyelven a this alapértelmezett paraméterváltozó valójában nem magát az objektumot, hanem csak egy arra mutató pointert jelöl. A pointerekről a 13. fejezetben részletesen is lesz szó, egyelőre erről elég annyit tudni, hogy emiatt egy metódusban a hívó objektumra *this kifejezéssel, az objektum egy tagjára pedig a this->tag kifejezéssel hivatkozhatunk, bár ez utóbbi helyett használhatjuk egyszerűen a tag kifejezést is. A metódusok törzsének helyét a programozási nyelvek eltérő módon jelölik. C++ nyelven a metódus törzseket lehet az osztály definíción belül is, de azon kívül is definiálni. Hatékonysági okból csak a rövid, néhány értékadást tartalmazó metódus törzseket érdemes úgynevezett inline definícióként, az osztály leírásban, közvetlenül a metódus deklarációja mögött megadni. Összetettebb metódustörzs esetén a külső definíciót ajánljuk. Ilyenkor a deklarációt meg kell ismételni, és a metódus neve elé az osztályának nevét is le kell írni (Tipus::). Egy osztálynak lehetnek privát metódusai is. (Ebben a tekintetben a ábra által sugallt osztályleírás, miszerint az adattagok a privátak, a metódusok a publikusak, nem pontos.) A privát metódusok csak ugyanazon osztály más metódusaiból hívhatóak meg. Az ilyen metódushívásnál nem kell a hívó objektumot megadni, hiszen az értelemszerűen a hívást tartalmazó metódus alapértelmezett objektuma lesz. 417

418 Azokat a metódusokat, amelyek nem változtatják meg a hívó objektumukat konstans metódusoknak nevezik. C++ nyelven ezt a metódus deklarációjának végén feltüntetett const szóval jelezhetjük. Minden osztálynak vannak speciális metódusai: az osztály nevével megegyező konstruktor (ebből több is lehet) és ennek ellentét párja, a destruktor. A konstruktornak és a destruktornak nincs visszatérési típusa, és ezt még jelölni sem kell (tehát nem írunk a deklarációjuk elejére void-ot sem). Mindkettő automatikusan hívódik meg: a konstruktor akkor, amikor egy objektumot létrehozunk, a destruktor akkor, amikor az objektum élettartama lejár. Destruktorból csak egy van, de konstruktorból több is lehet, amelyek paraméterlistáinak a paraméterek számában, típusában vagy sorrendjében különböznie kell. Ha elfelejtenénk definiálni konstruktort vagy destruktort, akkor hivatalból lesz az osztálynak egy üres paraméterlistájú üres törzsű alapértelmezés szerinti üres konstruktora illetve egy üres destruktora. Ha explicit módon definiálunk egy konstruktort, akkor ez az alapértelmezett konstruktort még akkor is törli, ha az új konstruktor paraméterlistája nem üres. Ilyenkor, ha szükségünk van egy üres konstruktorra is, akkor azt explicit módon definiálnunk kell. Az alábbi osztálynak két konstruktora van. class Tipus{ ; private: public: int n; std::string str; Tipus(){n = 0; str = "hello"; Tipus(int a, std::string b) { n = i; str = b; 418

419 Objektum létrehozásakor mindig az a konstruktor hajtódik végre, amelyiknek formális paraméterlistája illeszkedik a létrehozásnál feltüntetett aktuális paraméterlistához. A Tipus o forma alkalmazásakor az üres paraméterlistájú konstruktor hívódik meg, a Tipus o(12, "hello") esetén pedig az aktuális paraméterlista elemeinek száma, típusa és sorrendje alapján beazonosítható konstruktor. Az adattagok kezdeti értékadása az alábbi konstruktor segítségével is elvégezhető. Ennek hatása egyenértékű a fenti mutatott osztály második konstruktorával. Tipus(int a, std::string b):n(i),str(b) { A konstruktor feladata egyszerű osztály esetén az, hogy a létrehozandó objektum adattagjaihoz a típus-invariánst kielégítő kezdőértéket rendelje. A destruktor szerepe egyszerű osztályoknál az egyes adattagok megfelelő lezárása, amennyiben ehhez nem elegendő az adattagok destruktora, amelyek viszont automatikusan meghívódnak. Ennél fogva egyszerű osztályoknál gyakori, hogy nem írunk destruktort. Egy osztálynak az adattagokon és metódusokon kívül lehetnék még egyéb tagjai is, mint konstansok, típusdefiníciók, belső osztályok stb. Két különböző osztálynak lehet ugyanolyan nevű és típusú adattagja, ugyanolyan nevű és típusú metódusa. Sőt egy osztályon belül lehetnek azonos nevű metódusok is, ha azoknak eltér a típusa: paraméterlistája, visszatérési érték típusa és attribútumai (private/public, static, const). Ezt a többalakúságot az operátorokra is alkalmazhatjuk, azaz például új, az adott osztály objektumaihoz köthető jelentés adható például a + operátornak. (Erre majd mutatunk példákat.) Az osztály leírást a kódban kétféleképpen helyezhetjük el. Az első az, amikor az osztályt használó forrásállomány elején definiáljuk az osztályt, és ugyanebben a forrásállományban az egyéb függvény-definíciók között definiáljuk az osztály metódusait. A másik az, amikor az osztály leírást külön csomagba helyezzük. C++ nyelven az osztály-definíciót egy fejállományba, az ahhoz tartozó metódus-definíciókat pedig az ahhoz tartozó forrásállományba szoktuk tenni, és a fejállományt beinklúdoljuk mind a metódus-definíciókat 419

420 tartalmazó forrásállományba, mind az összes olyan forrásállományba, ahol az osztály szolgáltatásait használni akarjuk. Az összetartozó fejállományforrásállomány neve tradicionális megállapodás alapján az osztály nevével azonos, egyik esetben.h, a másik esetben.cpp kiterjesztésű. 420

421 Osztállyal kapcsolatos speciális nyelvi fogalmak Adattagok kezdeti értékadása A konstruktorok definíciójában a formális paraméter listát követő kettőspont után az adattagok kezdeti értékadásait hívhatjuk meg. Ehhez az érintett tagváltozókat kell felsorolni úgy, hogy a nevük mögötti zárójelben adjuk meg a nekik szánt kezdő értéket: Tipus::Tipus(): tag1( ), tag2( ), { Értékadás operátor, másoló konstruktor Minden osztály alapértelmezés szerint rendelkezik egy úgynevezett másoló konstruktorral (amely egy már létező objektum adattagjainak értékeivel hoz létre egy új objektumot) és egy értékadás operátorral (amely egy objektum adattagjaival felülírja egy másik objektum adattagjait). Tipus o2(o1); o1 = o2; // Tipus o1 már létezik Egyszerű osztályok használata esetén ezek az alapértelmezett metódusok megfelelően működnek, általános (dinamikus helyfoglalást végző) osztályok esetén azonban többnyire felül kell definiálni őket. Konstans metódus Olyan metódusok, amelyek nem változtatják meg a hívó objektumuk adattagjait. C++ nyelven ezt a metódus deklarációjának végén feltüntetett const szóval jelezhetjük. Ezt a tulajdonságot a fordító program ellenőrzi. Getter/Setter Egy privát adattag elérését biztosító publikus metódusok. A getter az adattag értékét (esetleg átalakítva) visszaadó konstans metódus, a setter az adattag értékét (kellő ellenőrzés mellett) felülíró metódus. Inline metódus A metódusnak az osztálydefiníción belül történő definiálása. Csak egyszerű, néhány értékadásból álló metódustörzs esetén alkalmazzuk. Barát alprogram, barát osztály Egy osztály barátjaként deklarálhat egy külső alprogramot vagy másik osztályt. Ennek hatására a barát alprogramban illetve osztályban láthatóak lesznek a deklaráló osztály privát elemei is. Ez tehát a private láthatóság 421

422 hatókörét kiterjesztő lehetőség. Osztály szintű elemek Egy osztályban lehetőség van (static jelző használatával) olyan adattagok illetve metódusok definiálására is, amelyek az osztály alapján létrehozott objektumoktól független elemek lesznek. A statikus adattagok többnyire az osztállyal kapcsolatos statisztikai adatok tárolására (hány objektum jött létre) alkalmasak. A statikus metódusoknak nincs alapértelmezett objektum paraméterük, de valamilyen módon kötődnek az osztályukhoz (például annak több objektumával végeznek műveletet). Ebben a tekintetben a lehetőségeik a barát alprogramokhoz hasonlóak. 422

423 27. Feladat: UFO-k Képzeljük el, hogy egy űrállomást állítanak Föld körüli pályára, amelynek az a feladata, hogy adott számú észlelt, de ismeretlen tárgy közül megszámolja, hány tartózkodik az űrállomás közelében (mondjuk km-nél közelebb hozzá). Specifikáció A feladat részletes elemzését már megtettük (I. kötet 7.1 példa), most csak annak eredményét idézzük fel. A = ( g : Gömb, v : Pont n, db : N ) Ef = ( g=g' v=v' ) 1. n Uf = ( Ef db 1 ) i 1 v[ i] g Gömb típus: gömb l:=p g g:gömb, p:pont, l: (c,r):pont R, r 0 l:=távolság(c,p) r p:pont, d:r, l: Pont típus: pont d:=távolság(p,q) p,q:pont, d:r (x,y,z): R R R d : ( p. x q. x) ( p. y q. y) ( p. z q. z) Absztrakt program 423

424 Ezt a feladatot legfelső szinten egy számlálás oldja meg. db := 0 i =1.. n v[i] g db := db + 1 SKIP Implementálás Az implementáció magán viseli mind a funkció vezérelt, mind a típus központú szemléletmódot. Mindezzel együtt ez már egy objektum orientált kódot eredményez. Komponens szerkezet A megoldó program kódját négy részre vágjuk. Külön csomagot alkot a Gömb típust (Sphere), külön a Pont típust (Point) megvalósító kód, külön csomagba kerülnek az egész és valós számok beolvasását segítő függvények (a ReadInt() és ReadReal() függvényekkel már a korábbi fejezetekben találkoztunk), a főcsomag pedig a main függvényt tartalmazza. A sphere.h és point.h csomagok kialakítása a típus orientáltság, a read.h-read.cpp pedig a funkció orientáltság jegyében történt. main.cpp sphere.h point.h read.h - read.cpp Sphere Point ReadInt() main() Sphere() In() Point() Set() Distance() ReadReal() Pos() ábra. Komponens szerkezet 424

425 425

426 Függvények hívási láncolata A vezérlést, azaz a komponensek egyes függvényeinek megfelelő sorrendben történő meghívását a main függvény biztosítja. Minden hívás vagy egy gömb (Sphere), vagy egy pont (Point) objektummal kapcsolatos tevékenységért felelős. ReadReal() Pos() ReadInt() main() Point() Set() Sphere() In() Distance() ábra. Alprogramok hívási lánca Gömb típus megvalósítása class Sphere{ private: Point c; double r; public: enum Errors { Negativ_Radius ; Sphere(const Point &p, double a) { if (a<0) throw Negativ_Radius; c=p; r=a; 426

427 bool In(const Point &p) { return c.distance(p)<=r; ; A Gömb típust a Sphere osztály valósítja meg. Tekintettel a metódusok egyszerű voltára, azokat inline módon, a definícióba ágyazva implementáltuk, ezért nem tartozik a sphere.h állományhoz sphere.cpp állomány is. A konstruktor negatív sugár esetén kivételt dob. Az itt implementált programban ez a kivétel ugyan nem következhet be, mert majd, mint látni fogjuk, a sugár értékének beolvasásánál ellenőrzést végzünk. Az In() metódus a Point osztály Distance() metódusát hívja, annak segítségével vizsgálja meg, hogy a megadott pont mennyire esik közel a gömb középpontjához. Pont típus megvalósítása A Pont típust a Point osztály valósítja meg. A metódusokat itt is inline módon implementáljuk. class Point{ private: double x,y,z; public: Point(){ x = y = z = 0.0; void Set(double a, double b, double c) { x = a; y = b; z = c; double Distance(const Point &p) const { 427

428 return sqrt(pow(p.x-x,2)+ pow(p.y-y,2) + pow(p.z-z,2)); ; A Point() konstruktor egy origóba pozícionált pontot hoz létre. Egy pontnak a pozícióját a Set() metódus segítségével tudjuk megváltoztatni. Felmerülhet a kérdés, miért nem definiálunk inkább olyan konstruktort is, amelyik egyből beállítaná a létrehozandó pont koordinátáit. Ennek az a magyarázata, hogy amikor majd egy vector<point> típusú tömböt hozunk létre a főprogramban, akkor ott a Point-nak csak egy paraméter nélküli konstruktorával jöhetnek létre a tömb elemei, és csak ezután tudjuk egyenként módosítani a tömbbeli pontok pozícióit. Önálló pontot csak egyszer kell létrehozni (a gömb középpontját), ennek kedvéért most nem definiálunk másik, koordinátákkal paraméterezhető konstruktort. (Ha majd megismerjük a pointerváltozó fogalmát, akkor lehetőség nyílik az ittenitől eltérő megoldásra is.) A Distance() metódussal nem két pont távolságát, hanem egy pontnak egy másiktól való távolságát számoljuk ki. E két értelmezés között az eredmény szempontjából ugyan nincs semmi különbség, de a nyelvi megvalósításukban már igen. Az első értelmezéshez jobban illeszkedne egy Distance(p,q) alakú hívás, de ekkor a Distance() nem lehetne a Point osztály metódusa, csak egy két Point típusú paraméterrel rendelkező függvény. A nyelvi szabályok miatt a Point osztály csak olyan metódust definiálhatunk, amelyet p.distance(q) alakban hívhatunk meg, amely a második értelmezést testesíti meg. A Distance()metódus konstans metódus, hiszen nem változtatja meg azt a pontot, amelyiktől vett távolságot számítja. A metódus törzse támaszkodik a cmath csomag szolgáltatásaira (gyökvonás, hatványozás). Főprogram kódolása 428

429 A main függvény először az űrállomás (a gömb) középpontjának koordinátáit olvassa be (ReadReal()), létrehozza a középpontot (Point()) és beállítja (Set()) a koordinátáit, majd a sugár beolvasása után magát a gömböt is megalkotja (Sphere()). double x,y,z,r; cout << "Az űrállomás koordinátái:\n"; x = ReadReal("\t x: ", "Valós számot várok!"); y = ReadReal("\t y: ", "Valós számot várok!"); z = ReadReal("\t z: ", "Valós számot várok!"); Point c; c.set(x,y,z); r = ReadReal("Űrállomás körzetének sugara: ", Sphere g(c,r); "Nem-negatív valós szám kell!", Pos); A sugár beolvasásához szükség van egy ellenőrző függvényre (Pos) is, amelyet a read csomag tartalmaz. Ez negatív valós számra hamis értéket ad vissza. Az űrállomást képviselő gömb létrehozását követi az azonosítatlan repülő objektumok beolvasása. Először megadott számú térbeli pontot tartalmazó vektort hozzuk létre. Ebben ekkor még csupa origóbeli pont van, hiszen a vektor deklarálásakor a Point osztály paraméter nélküli konstruktorát használjuk. Az egyes pontokat a koordináták beolvasása után módosítjuk (Set()). int n = ReadInt("UFO-k száma: ", "Természetes szám kell!"); 429

430 vector<point> v(n); for(int i=0; i<n; ++i){ cout << "Az " << i+1 << "-dik UFO koordinátái:\n"; x = ReadReal("\tx: ","Valós számot várok!"); y = ReadReal("\ty: ","Valós számot várok!"); z = ReadReal("\tz: ","Valós számot várok!"); v[i].set(x,y,z); Az absztrakt főprogram egy egyszerű számlálás, amely a benne van-e egy pont a gömbben műveletét hívja meg (In()). Ezt az eredmény kiírása követi. int db = 0; for(int i=0; i<n; ++i){ if(g.in(v[i])) ++db; cout << "Közeli UFO-k száma: " << db; 430

431 Tesztelés Fekete doboz tesztesetek: (a számlálás, annak intervalluma és különleges értékek tesztelése) 1. Nulla darab pont esete. 2. Nulla sugarú gömb esete benne levő ponttal. 3. Olyan gömb és pontok, ahol az első pont/utolsó pont esik csak a gömbbe. 4. Olyan adatok, hogy egyetlen pont sem esik a gömbbe. 5. Olyan adatok, hogy minden pont a gömbbe esik. 6. Általános eset. 7. Negatív sugarú gömb. A komponensek (osztályok) tesztelése nem túl bonyolult, hiszen a konstruktoron és értékadó (setter) metóduson kívül mindkét osztály egyetlen metódust tartalmaz csak, így a metódusok variációinak kipróbálására nincs szükség. 1. Sphere osztály tesztelése a. A Negativ_Radius kivétel dobásának tesztelése. b. Nulla sugarú gömb létrehozása. c. Egy gömb és egy a gömbbe eső pont vizsgálata. d. Egy gömb és egy a gömbön kívüli pont vizsgálata. e. Egy gömb és egy a gömb felületére eső pont vizsgálata. 2. Point osztály tesztelése. a. Pont létrehozása. b. Pont pozíciójának megváltoztatása c. Két pont távolsága (azonos, csak egy koordinátában eltérő, csak két koordinátában eltérő, mindhárom koordinátában eltérő pontokkal) d. A p és q pontok, illetve q és p pontok távolsága megegyezik-e (szimmetria)? 431

432 Végül a beolvasást végző függvények korábban már látott tesztelése következik. 432

433 Teljes program main.cpp: #include <iostream> #include <vector> #include "read.h" #include "point.h" #include "sphere.h" using namespace std; int main() { // Űrállomás beolvasása double x,y,z,r; cout << "Az űrállomás koordinátái:\n"; x = ReadReal("\t x: ", "Valós számot várok!"); y = ReadReal("\t y: ", "Valós számot várok!"); z = ReadReal("\t z: ", "Valós számot várok!"); Point c; c.set(x,y,z); r = ReadReal("Űrállomás körzetének sugara: ", "Nem-negatív valós számot várok!", Pos); Sphere g(c,r); // UFO-k beolvasása int n = ReadInt("UFO-kszáma: ", 433

434 "Természetes számot várok!"); vector<point> v(n); for(int i=0; i<n; ++i){ cout << "Az " << i+1 << "-dik UFO koordinátái:\n"; x = ReadReal("\tx: ","Valós számot várok!"); y = ReadReal("\ty: ","Valós számot várok!"); z = ReadReal("\tz: ","Valós számot várok!"); v[i].set(x,y,z); // Számlálás int db = 0; for(int i=0; i<n; ++i){ if(g.in(v[i])) ++db; // Kiírás cout << "Közeli UFO-k száma: " << db; char ch; cin>>ch; return 0; sphere.h: #ifndef SPHERE_H #define SPHERE_H 434

435 #include "point.h" class Sphere{ private: Point c; double r; public: enum Errors { Negativ_Radius ; Sphere(const Point &p, double a) { if (a<0) throw Negativ_Radius; c=p; r=a; bool In(const Point &p) { return c.distance(p)<=r; ; #endif point.h: #ifndef SPHERE_H #define SPHERE_H 435

436 #include <cmath> class Point{ private: double x,y,z; public: Point(){ x = y = z = 0.0; void Set(double a, double b, double c) { x = a; y = b; z = c; double Distance(const Point &p) const { return sqrt(pow(p.x-x,2)+ pow(p.y-y,2) + pow(p.z-z,2)); ; #endif 436

437 read.h: #ifndef _READ_H #define _READ_H #include <string> bool All(int n){ return true; bool All(double r){ return true; bool Pos(double a){ return a>=0.0; int ReadInt (std::string msg, std::string errormsg, bool cond(int) = All); double ReadReal(std::string msg, std::string errormsg, bool cond(double) = All); #endif read.cpp: #include "read.h" #include <iostream> using namespace std; int ReadInt(string msg, string errmsg, bool check(int) ) { int n; int error = true; do{ cout << msg; cin >> n; 437

438 if(error = cin.fail()!check(n)){ cout << errmsg << endl; cin.clear(); string tmp; getline(cin,tmp); while(error); return n; double ReadReal(string msg, string errmsg, bool check(double)) { double a; bool error = true; do{ cout << msg; cin >> a; if(error = cin.fail()!check(n)){ cout << errmsg << endl; cin.clear(); string tmp; getline(cin,tmp); while(error); return a; 438

439 28. Feladat: Zsák Adott egy n, de legalább 1 hosszúságú legfeljebb 0 és 99 közé eső egész számokat tartalmazó tömb. Melyik a tömbnek a leggyakrabban előforduló eleme! Specifikáció A feladat részletes elemzésével már találkoztunk (I. kötet 7.2 példa), most csak annak eredményét idézzük fel. A = ( t : {0..99 n, b : Zsák, e : {0..99 ) Ef = ( t = t n 1 ) Uf = ( Ef b n { t[ i] e = MAX(b) ) i 1 Zsák típus: zsák b:= b:zsák b:= b {e e:{0..99 e:=max(b) v: e [0..99]: v[e]:=0 v[e]:=v[e]+1 e:{0..99 Absztrakt program A feladatot egy olyan összegzés oldja meg, ahol az összeadás műveletét a zsákunió művelete helyettesíti. Ezt a zsák maximális előfordulás számú elemének kiválasztása követi. A MAX(b) értelmes, mivel a feladat előfeltétele garantálja, hogy legalább egy elem be fog kerülni a zsákba. 439

440 b := i =1..n b := b {t[i] e := MAX(b) Implementálás Az implementálás egy objektum orientált kódot eredményez. Komponens szerkezet A megoldásban külön csomagba kerül a Zsák típus (Bag) és külön csomagba a főprogram, amely a main függvény mellett a feladatban szereplő bemeneti tömb feltöltését végző Read() függvényt is tartalmazza. main() Read() main.cpp Bag bag.h - bag.cpp Bag() Put() Max() ábra. Komponens szerkezet Függvények hívási láncolata A vezérlést, azaz a komponensek egyes függvényeinek megfelelő sorrendben történő meghívását a main függvény biztosítja. 440

441 Read() main() Bag() Put() Max() ábra. Alprogramok hívási lánca Zsák típus megvalósítása A Zsák típust megvalósító Bag osztályt a bag.h állományban definiáljuk. Ez a tervben bevezetett három metódus mellett két kivételt is dobhat. A WrongInput kivétel csak a Put()metódusban keletkezhet, az EmptyBag csak a Max() metódusban. class Bag{ public: enum Errors{WrongInput, EmptyBag; Bag(); void Put(int e); int Max() const; private: static const int n = 100; int v[n]; ; 441

442 A reprezentációban bevezetjük a 100-as értéket helyettesítő konstanst. Ennek az előnye az, hogy amennyiben ez az érték változna, akkor ezt elég egyetlen helyen módosítani. Ezt egy statikus konstansként hozzuk létre, így ez nem egyetlen zsákra, hanem a Bag osztályból létrehozható összes zsákra (bár ebben a feladatban csak egyetlen zsákot használunk) egyaránt érvényes. A metódusok megvalósítását elkülönítve a bag.cpp állományba helyezzük el. A metódusok törzse a terv alapján készült, kiegészülve a kivételek dobásával. A Put() metódus akkor dob WrongInput kivételt, ha nem 0 és 99 közötti számot akarunk a zsákba betenni. A Max() konstans metódus akkor dob EmptyBag kivételt, ha a zsák üres. #include "bag.h" using namespace std; Bag::Bag() { for (int k=0; k<n; ++k) v[k] = 0; void Bag::Put(int e) { if (e<0 e>n-1) throw WrongInput; ++v[e]; int Bag::Max() const { int max = v[0]; int e = 0; for (int k=1; k<n; ++k) 442

443 if (v[k]> max){ max = v[k]; e = k; if (0 == max) throw EmptyBag; return e; Főprogram kódolása A main függvény először deklarálja a t bementi tömböt és létrehozza a zsákot, majd feltölti a bemeneti tömböt a Read() függvény segítségével. Megjegyezzük, hogy a tömb a megvalósításban 0-tól indexelt. Ezt követően a tömb elemeit egyenként betesszük a zsákba. Az utolsó lépés a leggyakoribb elem kiválasztása és kiírása. Mivel előfordulhat, hogy a zsákba betenni kívánt elem nem 0 és 99 közé esik, ezért a Put() műveletet kivételkezelésnek vetjük alá. Lekezeljük az EmptyBag kivételt is, amely akkor következhet be, ha a bemeneti tömb üres volt vagy nem tartalmazott 0 és 99 közötti elemet, azaz a zsák üres maradt. Ezen a ponton általánosítottuk a tervet, hiszen ott feltettük, hogy a bemeneti tömb nem üres és csupa 0 és 99 közötti számot tartalmaz. A bementi tömb feltöltése egy olyan szöveges állományból történik, amelyik első adata a tömb elemszámát, az azt követő adatai pedig a tömb elemeit tartalmazza. Feltesszük, hogy ezek mind egész számok, és a tömb hosszát megadó szám sem negatív. vector<int> t; Read(t); Bag b; for(int i=0;i<(int)t.size();++i){ try{ b.put(t[i]); 443

444 catch(bag::errors ex){ if(bag::wronginput == ex) cout << "Hibás adat a tömbben!\n"; try{ cout << "Leggyakoribb elem: " << b.max(); catch(bag::errors ex){ if(bag::emptybag == ex) cout << "Üres tömb!\n"; 444

445 Tesztelés Fekete doboz tesztesetek: Érvénytelen esetek: 1. Üres bemeneti tömb 2. Egyetlen, nem 0 és 99 közötti számot tartalmazó bementi tömb. 3. Több, nem 0 és 99 közötti számot tartalmazó bementi tömb. 4. Csupa, nem 0 és 99 közötti számot tartalmazó bementi tömb. Érvényes esetek: 1. Egyetlen 0 és 99 közötti számot tartalmazó bementi tömb. 2. Olyan bemeneti tömb, amely első/utolsó eleme a nulla, ezen kívül még egy nullát tartalmaz, minden más számból legfeljebb egyet. 3. Olyan bemeneti tömb, amely első/utolsó eleme a 99, ezen kívül még egy 99-t tartalmaz, minden más számból legfeljebb egyet. 4. Olyan bemeneti tömb, amelyben több szám is egyforma gyakorisággal található. A főprogram fehérdoboz tesztelése sem igényel újabb teszteseteket, a bemeneti tömb feltöltése, az elemek zsákba pakolása a fekete doboz tesztesetekkel már ki lett próbálva. Komponens teszt. Külön tesztelendő a Bag osztály. Elsősorban a Max() művelet tesztelésére kell figyelnünk (amikor a zsák első vagy utolsó eleme a legnagyobb számosságú, vagy több egyformán maximális számosság is van), de ezt a fenti érvényes esetek lefedik. Ezen kívül a kivétel dobásokat és azok kezelését kell még ellenőrizni. A metódusok variációinak tesztje itt nem kell. 445

446 Teljes program main.cpp: #include <iostream> #include <fstream> #include <vector> #include <cstdlib> #include "bag.h" using namespace std; void Read(vector<int> &t); int main() { vector<int> t; Read(t); Bag b; for(int i=0;i<(int)t.size();++i){ try{ b.put(t[i]); catch(bag::errors ex){ if(bag::wronginput == ex) cout << "Hibás adat a tömbben!\n"; 446

447 try{ cout << "Leggyakoribb elem: " << b.max(); catch(bag::errors ex){ if(bag::emptybag == ex) cout << "Üres tömb!\n"; char ch; cin >> ch; return 0; void Read(vector<int> &t) { ifstream f("input.txt"); if (f.fail()) { cout << "Hibás fájlnév!\n"; exit(1); ; int n; f >> n; t.resize(n); for (int i=0;i<n;++i){ f >> t[i]; bag.h: #ifndef BAG_H 447

448 #define BAG_H #include <vector> class Bag{ public: enum Errors{WrongInput, EmptyBag; Bag(); void Put(int e); int Max() const; private: static const int n = 100; int v[n]; ; #endif bag.cpp: #include "bag.h" using namespace std; 448

449 Bag::Bag() { for (int k=0; k<n; ++k) v[k] = 0; void Bag::Put(int e) { if (e<0 e>n-1) throw WrongInput; ++v[e]; int Bag::Max() const { int max = v[0]; int e = 0; for (int k=1; k<n; ++k) if (v[k]> max){ max = v[k]; e = k; if (0 == max) throw EmptyBag; return e; 449

450 29. Feladat: Síkvektorok Adott n+1 darab síkvektor. Igaz-e, hogy az első n darab síkvektor összege merőleges az n+1-edik síkvektorra? Specifikáció A feladat megoldásához ki kell számolnunk az n darab síkvektor eredőjét (összegzés), majd ennek az n+1-edik vektorral vett skaláris szorzatát. Ha ez nulla, akkor merőleges az eredő az n+1-edik síkvektorra. A = ( t : Síkvektor n, v : Síkvektor, l : ) Ef = ( t=t' v=v' ) Uf = ( Ef s n 1 l = (s*v=0.0) ) i 1 A megoldáshoz definiálnunk kell a síkvektorok típusát. A síkvektorokat origóból induló helyvektorokként ábrázoljuk, és a végpontjuk koordinátáival reprezentáljuk. Öt műveletet vezetünk be: a nullvektor létrehozását, egy vektor végpontjának egyik illetve másik koordinátájának megváltoztatását, egy vektorhoz egy másik vektor hozzáadását és két vektor skaláris szorzását. Síkvektor típus: síkvektor v := nullvektor v:síkvektor v.setx(a), v.setyb() v := v + v 2 d := v 1 * v 2 v 2 :Síkvektor a,b: R v 1,v 2 :Síkvektor, d: R (x, y): R R x, y := 0.0, 0.0 x := a; y := b a,b: R x,y := x + v 2.x, y + v 2.y d := v 1.x*v 2.x + v 1.y*v 2.y d: R 450

451 Absztrakt program A feladatot egy összegzés, majd a skaláris szorzás eredményének vizsgálata oldja meg. s := nullvektor i =1.. n s := s + t[i] l := s*v=0.0 Implementálás Az implementálás során elkészítjük a Síkvektor típust, majd kódoljuk a fenti absztrakt programot. Komponens szerkezet A megoldó program kódját három részre vágjuk. Külön csomagot alkot a Síkvektor típusát megvalósító Vector2D osztály, külön csomagba kerülnek az egész, a természetes és a valós számok beolvasását segítő függvények (a ReadInt() és ReadReal() függvények), a főprogram pedig a main függvényt tartalmazza. A Vector2D osztály operator+=() metódusa a v := v + v 2 hozzáadás műveletét, az operator*() pedig a d := v 1 * v 2 skaláris szorzást valósítja meg. A SetX() és SetY() egy vektor koordinátáinak módosítására szolgáló metódusok. Két konstruktort is bevezetünk. Az egyik a nullvektort létrehozó üres paraméterlistájú konstruktor, a másik, egy adott koordinátájú pontba mutató origó kezdetű síkvektort hoz létre. A főprogram Fill() eljárása síkvektorokkal tölt fel egy tömböt, a Sum() függvény pedig kiszámolja ezek eredőjét. 451

452 main.cpp main() Fill() Sum() vector2d.h - vector2d.cpp Vector2D Vector2D() Vector2D(a,b) SetX(),SetY() operator+=() operator*() read.h - read.cpp ReadInt() Nat() ReadReal() ábra. Komponens szerkezet Függvények hívási láncolata A vezérlést, azaz a komponensek egyes függvényeinek megfelelő sorrendben történő meghívását a main függvény biztosítja. Vector2D() Fill() ReadInt() ReadReal() SetX(), SetY() Nat() main() Sum() ReadReal() Vector2D(,) operator*() Vector2D() operator+=() ábra. Alprogramok hívási lánca 452

453 Főprogram kódolása A main függvény először a síkvektorok tömbjét hozza létre, amelyet a Fill() függvény segítségével tölt fel, majd a Sum() függvény segítségével kiszámolja a síkvektorok összegét. A külön síkvektor beolvasása illetve létrehozása után pedig ennek és az összegnek a skaláris szorzatát számolja ki, és az eredményt összeveti a 0.0-val. cout << "Összeadandó vektorok:" << endl; vector<vector2d> t; Fill(t); Vector2D s = Sum(t); cout << "Külön vektor:" << endl; Vector2D v( ReadReal("x = "), ReadReal("y = ")); if(s*v == 0.0) cout << "Merőleges"; else cout << "Nem merőleges."; A Fill() eljárás megadott méretre módosítja a paraméterként kapott tömböt, majd mivel ebben csupa nullvektor van a felhasználó adatai alapján módosítja a tömb összes síkvektorának koordinátáit. A ReadReal() alprogram olyan megvalósítását igényli az alábbi kód, amelyik második és harmadik paramétere is rendelkezik alapértelmezett értékkel, így hívásakor elég csak az első paraméterét megadni. void Fill(vector<Vector2D> &t) 453

454 { t.resize(readint("hány vektort fogsz megadni? ", "Természetes számot kérek!", Nat())); for(int i=0; i<(int)t.size(); ++i){ t[i].setx(readreal("x = ")); t[i].sety(readreal("y = ")); A Sum() függvény a tömb síkvektorait összegzi. Vector2D Sum(const vector<vector2d> &t) { Vector2D s; for(int i=0; i<(int)t.size(); ++i){ s+=t[i]; return s; Síkvektor típus megvalósítása A Síkvektor típust a vector2d.h-ban, a hozzáadás és skaláris szorzás metódusait a vector2d.cpp-ben definiáljuk. class Vector2D{ 454

455 private: double x,y; public: Vector2D():x(0),y(0){ Vector2D(double a, double b):x(a),y(b){ void SetX(double r){ x = r; void SetY(double r){ y = r; Vector2D operator+=(const Vector2D &v2); friend double operator*(const Vector2D &v1, const Vector2D &v2); ; Egy vektorhoz egy másikat hozzáadó metódust definiálhattuk volna a void Add(const Vector2D& v2) metódussal is (amit v1.add(v2)-ként hívnánk meg), de inkább a += operátor felüldefiniálását választottuk. Ezt a Vector2D operator+=(const Vector2D &v2) metódust ugyanis a v1 += v2 utasítás segítségével hívhatjuk majd meg, ami sokkal szemléletesebben fejezi ki a metódus tevékenységét. Két síkvektor skaláris szorzását nem lenne szerencsés az osztály metódusaként, tehát double Scalar(const Vector2D &v2)-ként definiálni, mert ez kiemelt helyzetbe kényszerítené az egyik vektort, azt, amelyikre, mint hívó objektumra, meg kellene majd hívni a metódust. Ekkor ugyanis egy d = v1.scalar(v2) hívást kellene alkalmazni, ami láthatóan nem szimmetrikus a két vektorra nézve. Ha azonban a double operator*(const Vector2D &v2) operátor felüldefiniálást használnánk, akkor ez a d = v1*v2 utasítással hívható, ami már sokkal tetszetősebb. 455

456 Hasonlóan szimmetrikus megoldást kínál a friend double Scalar(const Vector2D &v1, const Vector2D &v2) alkalmazása. Ez ugyan nem az osztály metódusa, de a friend tulajdonság miatt ugyanúgy hivatkozhat a törzsében az osztály privát adattagjaira, mint a metódusok, ennél fogva az osztály külső metódusának tekinthető. Nem hívó objektummal aktiváljuk, hanem a d = Scalar(v1,v2) hívással, de a paraméterek között szerepel legalább egy (itt kető) vektor típusú objektum. Ugyanezt operátorként bevezetve a friend double operator*(const Vector2D &v1, const Vector2D &v2) is biztosítja, amely a d = v1*v2 utasítással hívható. Annak érdekében, hogy a megoldásunkban legyen operátor felüldefiniálás és barát függvény is, ez utóbbi változatot használjuk. Vector2D Vector2D::operator+=(const Vector2D &v) { x+=v.x; y+=v.y; return *this; double operator*( const Vector2D& v1, const Vector2D &v2) { return v1.x*v2.x + v1.y*v2.y; Tesztelés A Vector2D komponens teszteje: A teszteléshez célszerű az osztályban egy olyan metódust is készíteni, amelyik meg tud jeleníteni egy síkvektort (kiírja a koordinátáit). Ezek után készítünk egy tesztprogramot (egy menüt), amely tetszőleges sorrendben hívhatja az osztály metódusait. 456

457 1. Konstruktor teszt. (Létrejön-e a nullvektor?) 2. SetX() és SetY() tesztje. (Megváltozik egy síkvektor koordinátája?) 3. Hozzáadás műveletének tesztje. (Egy vektorhoz hozzáadni a nullvektort, az egységvektorokat, egy tetszőleges vektort.) 4. Skalárszorzás tesztje. (nullvektorok szorzása, nem nullvektorok szorzása, kommutatívitás) Variációs teszt (a metódusok különféle sorrendben történő kipróbálása) itt nem kell. Fekete doboz tesztesetek: A Sum()-beli összegzésre és a vizsgált vektor, a tömbbeli vektorok eredőjének merőlegességre vonatkozó tesztek, illetve a különleges adatok (nulla, egy, negatív számok, törtek). 1. A vizsgált vektor nullvektor (0.0, 0.0) és üres a vektortömb. Válasz: merőleges. 2. A vizsgált vektor nullvektor és nem üres vektortömb. Válasz: merőleges. 3. A vizsgált vektor nem nullvektor és a vektortömb eredője nullvektor ([(0.0, 0.0)] vagy [(3.0,3.0), (-3.0,-3.0)]). Válasz: merőleges. 4. A vizsgált vektor (3.0, 3.0) és a vektortömb [(-3.0, 3.0)]. Válasz: merőleges. 5. A vizsgált vektor (3.0, 3.0) és a vektortömb [(-3.5, -3.71)]. Válasz: nem merőleges. 6. A vektortömb első/utolsó eleme nullvektor, de a vizsgált síkvektor az eredőre nem merőleges. 457

458 Fehér doboz tesztesetek: A fenti esetek a Sum() függvényt kielégítően tesztelik. Itt tehát csak a Fill() függvényt kell még tesztelni: hibás adatok (pl. negatív darabszám) bevitele. 458

459 Teljes program main.cpp: #include <iostream> #include <string> #include <vector> #include "vector2d.h" #include "read.h" using namespace std; void Fill(vector<Vector2D> &t); Vector2D Sum(const vector<vector2d> &t); int main() { cout << "Összeadandó vektorok:" << endl; vector<vector2d> t; Fill(t); Vector2D s = Sum(t); cout << "Külön vektor:" << endl; Vector2D v( ReadReal("x = ",""), ReadReal("y = ","")); if(s*v == 0.0) cout << "Merőleges"; 459

460 else cout << "Nem merőleges."; char ch; cin >> ch; return 0; void Fill(vector<Vector2D> &t) { t.resize(readint("hány vektort fogsz megadni? ", "Természetes számot kérek!", Nat)); for(int i=0; i<(int)t.size(); ++i){ t[i].setx(readreal("x = ","")); t[i].sety(readreal("y = ","")); Vector2D Sum(const vector<vector2d> &t) { Vector2D s; for(int i=0; i<(int)t.size(); ++i){ s+=t[i]; return s; vector2d.h: #ifndef VECTOR2D_H #define VECTOR2D_H 460

461 class Vector2D{ private: double x,y; public: Vector2D():x(0),y(0){ Vector2D(double a, double b):x(a),y(b){ void SetX(double r){ x = r; void SetY(double r){ y = r; Vector2D operator+=(const Vector2D &v2); friend double operator*(const Vector2D &v1, const Vector2D &v2); ; #endif vector2d.cpp: #include "vector2d.h" using namespace std; Vector2D Vector2D::operator+=(const Vector2D &v) { x+=v.x; y+=v.y; return *this; 461

462 double operator*( const Vector2D& v1, const Vector2D &v2) { return v1.x*v2.x + v1.y*v2.y; read.h: #ifndef READ_H #define READ_H #include <string> bool ci(int k); bool cd(double k); bool Nat(int n); int ReadInt(std::string msg, std::string errmsg = "", bool check(int) = ci); double ReadReal(std::string msg, std::string errmsg = "", bool check(double) = cd); #endif read.cpp: #include "read.h" 462

463 #include <iostream> using namespace std; bool ci(int k){ return true; bool cd(double k){ return true; bool Nat(int n) { return n >= 0; int ReadInt(string msg, string errmsg, bool check(int) ) { int n; int error = true; string tmp; do{ cout << msg; cin >> n; if(cin.fail()!check(n)){ cout << errmsg << endl; cin.clear(); getline(cin,tmp); while(error); return n; double ReadReal(string msg, string errmsg, 463

464 bool check(double)) { double a; bool error = true; string str; do{ cout << msg; cin >> str; a = atof(str.c_str()); error = 0 == a && str!= "0"; if(error) cout<< errmsg<< endl; while(error); return a; 464

465 C++ kislexikon osztály class{ private: int i; static const int n = 3; public: Tipus(); Tipus( ); ~Tipus(); void method1( ); void method2( ) const; void method3( ){ Tipus operator+=(const Tipus &v); friend void method4( ); ; Tipus::Tipus(){ Tipus::Tipus(int a):a(i){ Tipus::~Tipus(){ void Tipus::method1( ){ void Tipus::method2( ) const { Tipus Tipus::operator+=(const Tipus &p){ void method4( ){ 465

466 láthatóság konstruktor kezdeti értékadás destruktor konstans tag hívás konstans metódus inline operátor túlterhelés barát private, public Tipus(); Tipus( ); Tipus::Tipus(int a):a(i){ ~Tipus(); static const int n = 3; t.method( ); void method2( ) const; void method3( ) { Tipus operator+=(const Tipus &p); // hívása: t += t1; friend void method4( ); 466

467 12. Felsorolók típusainak megvalósítása Könyvünk első kötetében számos feladat megoldását terveztük a felsorolókra általánosított programozási tételekre történő visszavezetés segítségével. Az ilyen megoldások implementálásában az egyedüli problémát az alkalmazott felsoroló megvalósítása jelentheti. Néha a felsoroló típusa meglévő típusok valamelyikével kiváltható, máskor azonban egy saját osztályt kell ehhez definiálni. Implementációs stratégia A felsoroló egy olyan objektum, amely bizonyos elemek felsorolását teszi lehetővé. A felsorolandó elemek elhelyezkedhetnek közvetlenül egy gyűjteményben (tárolóban), és ilyenkor a felsorolónak nem kell mást tennie, mint bejárni a tárolt elemeket. Máskor viszont a felsorolni kívánt elemek nem állnak explicit módon rendelkezésünkre, azokat elő kell állítani, ki kell számolni. Igaz, hogy a felsoroló használata ilyenkor is azt az illúziót kelti, mintha az általa felsorolt elemek sorozata valóban létezne, de ilyenkor a felsoroló a valóságtól elvonatkoztatott, absztrakt objektum. t.first() t.end() feldolgozás(t.current()) t.next() 12-1 Felsoroló által szolgáltatott elemek feldolgozása Függetlenül azonban attól, hogy a felsoroló konkrét elemeket jár-e be vagy csak generálja a felsorolt elemeket, a felsoroláshoz minden esetben ugyanaz a négy művelet szükséges. A First() művelet indítja el a felsorolást azzal, hogy rááll a felsorolás során először érintett elemre feltéve, hogy van ilyen. Minden további, tehát soron következő elemre a Next() művelet 467

468 segítségével tudunk ráállni. A Current() művelet a felsorolás alatt kijelölt aktuális elemet (amire éppen ráállt a felsorolás) adja vissza. Az End() a felsorolás során mindaddig hamis értéket ad, amíg van kijelölt aktuális elem, a felsorolás végét viszont igaz visszaadott értékkel jelzi. First(): Next(): Current(): End(): rááll az első felsorolandó elemre rááll a rákövetkezendő felsorolandó elemre visszaadja a felsorolás aktuális elemét akkor ad igazat, ha nincs már több felsorolni kívánt elem ábra. Egy felsoroló műveletei Egy felsoroló megvalósítása elsősorban a műveleteinek implementálását jelenti. Megfigyelhető, hogy amikor a felsorolás egy gyűjtemény elemeinek bejárása, akkor a felsoroló műveleteit a gyűjtemény műveleteivel közvetlenül helyettesíthetjük: ilyenkor a felsoroló típusát nem szükséges külön osztállyal megvalósítani. Egy intervallum bejárásához például elég egy egész típusú változót használunk felsorolóként, ennek a változónak könnyen lehet kezdőértéket adni (First()), tudjuk növelni az értékét (Next()), képesek vagyunk vizsgálni, hogy elért-e már egy kívánt értéket (End()), és az aktuális elem maga a változó értéke (Current()). Az ilyen felsoroló tehát rendelkezésünkre áll, előállítása különösebb erőfeszítéseket az implementáció során nem igényel. Egy szekvenciális inputfájl elemeinek bejárása is egyszerűen megoldható. Ennek jelentősége nagy, hiszen sokszor kell például szöveges állománybeli adatokat szekvenciális inputfájlként kezelni. A szekvenciális inputfájlokra a read műveletet szokás megvalósítani. Emlékeztetünk arra, hogy az első kötetben az olvasást az st,e,f:=read(f) értékadással, vagy rövidítve az st,e,f:read szimbólummal jelöltük, ahol f a fájlt, e a kiolvasott elemet, az st az olvasás státuszát (Státusz ={abnorm, norm) azonosítja. Ha az f eredeti értéke egy üres sorozat, akkor az olvasás után az st változó az abnorm értéket veszi fel, az f-beli sorozat továbbra is üres marad, az e pedig definiálatlan. Ha az f-beli eredeti sorozat nem üres, akkor az st változó értéke 468

469 norm lesz, az e az eredeti sorozat első elemét, az f az eggyel rövidebb sorozatot veszi fel értékként. A szekvenciális inputfájl read műveletével kiváltható a felsorolás klasszikus négy művelete. A fájl elemeinek felsorolása ugyanis előre-olvasási technikával történik (12-3. ábra), amelyből látszik, hogy a First() és a Next() műveleteket a read művelet helyettesíti, a Current() műveletet a read által beolvasott elem (e), az End() művelet pedig az end of file eseményt a read által beolvasott státuszt (st) segítségével vizsgálja. st,e,f : read st = norm feldolgozás(e) st,e,f : read ábra. Előre-olvasási technika Amikor a felsorolni kívánt elemek egy elképzelt sorozatot alkotnak, azaz nem léteznek a valóságban, akkor többnyire külön osztállyal célszerű definiálni a felsoroló típusát, amelyben külön metódusként szerepelnek a felsorolás alapműveletei. Gyakori, hogy a First() művelet abban különbözik csak a Next()-től, hogy néhány inicializáló lépést is tartalmaz: ilyenkor a First() meghívja a Next() metódust. Mivel a Current() és az End() műveleteknek nem szabad megváltoztatni a felsorolás állapotát, ezért ajánlott őket konstans metódusként definiálni. Így e műveleteket akárhányszor meghívhatjuk anélkül, hogy a felsorolást befolyásolnánk. Érdemes továbbá a Current() és az End() metódusokat konstans műveletigényűre készíteni úgy, hogy a felsorolás aktuális elemét illetve a felsorolás végét jelző logikai értéket adattagként vesszük fel a felsoroló osztályába, és a Current() valamint az End() csak ezen adattagok értékét adják vissza. A fentieken kívül az osztálynak adattagja lesz a felsorolást generáló adat is. Ilyen adat például egy természetes szám, ha annak prím osztóit kell felsorolni, vagy egy bitsorozat, ha annak nyolcbitnyi szakaszaiban tárolt egész számok felsorolása a feladat. 469

470 A felsoroló műveletek implementálásánál figyelembe vehetjük (mert ez könnyítést jelent), hogy a műveletek hatását nem kell minden esetre definiálni. Például nem-definiált az, hogy a First() végrehajtása előtt (tehát a felsorolás megkezdése előtt) illetve az End() igazra váltása után (azaz a felsorolás befejezése után) mi a hatása a Next() és a Current() műveleteknek. Általában nem definiált az sem, hogy mi történjen akkor, ha a First() műveletet a felsorolás közben ismételten végrehajtjuk, vagy az End() műveletet még az előtt használjuk, hogy a felsorolás a First() művelettel elindult volna. A felsoroló felhasználási módja, a felsorolókra épülő programozási tételek garantálják, hogy ezen hiányosságok ne okozzanak futási hibát. Hasonlóképpen azt sem e műveletek implementálásának kell garantálnia, hogy egy felsorolás véges lépésben biztosan befejeződjön. Egy felsoroló osztály komponens tesztje speciális, egyszerűsített eljárással történik, mivel a felsoroló műveleteinek egymáshoz való sorrendjét a hívó program garantálja, ennél fogva nincs szükség a metódusok variációinak tesztelésére, csak külön-külön az egyes metódusokéra. Nyelvi háttér A felsoroló típusának megvalósítását történjen az saját osztállyal vagy meglévő típusokkal nem igényel az eddig használtakhoz képest újabb nyelvi elemeket. Egy szekvenciális inputfájl elemeinek felsorolásához és feldolgozásához egyetlen read műveletet szoktunk használni. Ennek programozási nyelvi változatai azonban többnyire nem adják vissza közvetlenül azt az információt, hogy elértük-e már a felsorolás során a fájlvégét. A C-szerű nyelvekben a sikertelen olvasás (amikor a fájl elemeit már mind felsoroltuk, azaz a fájl kiürült) nem számít hibás műveletnek, végrajtását akár többször is megismételhetjük. Az olvasás után le tudjuk kérdezni azt, hogy az sikerült-e vagy sem (eof vagy fail). (Ettől lényegesen eltér a Pascal nyelv: ott előbb kérdezzük, hogy vajon nincs-e még fájlvége, és csak pozitív válasz esetén szabad olvasni.) f >> e; while(!f.fail()){ feldolgozás(e); f >> e; 470

471 12-4. Előre-olvasási technika egy C++ nyelvi változata A read művelet konkrét megjelenési formája a C++ nyelvben is függ a szekvenciális inputfájl fizikai formájától és a beolvasandó adat típusától. Mivel gyakori, hogy szöveges állományra épített szekvenciális inputfájl elemeit soroljuk és dolgozzuk fel, ezért érdemes átismételni (lásd 5. fejezet), hogy ezt miképpen lehet megtenni attól függően, hogy szöveges állomány milyen formában tartalmazza az adatokat. Szöveges állomány tartalmának karakterenkénti olvasását figyelhetjük meg az alábbi kódrészletekben. Ebben a beolvasott karaktereket rögtön ki is írjuk egy másik szöveges állományba, azaz itt egy karakterenkénti másolást látunk. ifstream x("inp.txt"); ofstream y("out.txt"); char ch; for(x.get(ch);!x.fail(); x.get(ch)){ y.put(ch); // lehetne y << ch is vagy ifstream x("inp.txt"); ofstream y("out.txt"); char ch; x.unsetf(ios::skipws); for(x >> ch;!x.fail(); x >> ch){ y << ch; // lehetne y.put(ch) is 471

472 A gyakorlatban a fenti kódok helyett az alábbiakat szokták alkalmazni. ifstream x("inp.txt"); ofstream y("out.txt"); char ch; while(x.get(ch)){ y.put(ch); vagy ifstream x("inp.txt"); ofstream y("out.txt"); char ch; x.unsetf(ios::skipws); while(x >> ch){ y << ch; A szöveges állománynak egynél több karakterből álló részét is be tudjuk olvasni egy adatként, és egyetlen olvasó utasítás segítségével. Az alábbi kóddal szöveges állományból elválasztó jelekkel szeparált egész számokat olvasunk be, és megszámoljuk a páros számokat. ifstream x("inp.txt"); int n, db = 0; for(x >> n;!x.fail(); x >> n){ if(n%2 == 0) ++db; vagy ifstream x("inp.txt"); int n, db = 0; while(x >> n){ 472

473 if(n%2 == 0) ++db; Technikailag semmi újdonságot nem tartalmaz az előzőekhez képest a következő kódrészlet. Ez egy szöveg szavainak átlagos szóhosszát számolja ki. Ebben elválasztó jelekkel (szóköz, tabulátor jel, sorvége jel) határolt sztringeket (szavakat) olvasunk. ifstream x("inp.txt"); string str; int db, hossz; db = hossz = 0; for(x >> str;!x.fail(); x >> str){ hossz += str.size(); ++db; int atl = hossz/db; vagy ifstream x("inp.txt"); string str; int db, hossz; db = hossz = 0; while(x >> str){ hossz += str.size(); ++db; 473

474 int atl = hossz/db; A szöveges állományok olvasásának harmadik lehetősége a soronkénti olvasás. Ha egy sorban szereplő adatokat elválasztó jelekkel határolják, de egy adaton belül nincs elválasztó jel, akkor a fenti technika ilyenkor is alkalmazható. Az alábbi kódrészletben sorszám-név párokat olvasunk a szöveges állományból. (Ez a program akkor is működik, ha a párok nem soronként helyezkednek el a szöveges állományban.) ifstream x("inp.txt"); int szam; string nev; for(x >> szam >> nev;!x.fail(); x >> szam >> nev){... vagy ifstream x("inp.txt"); int szam; string nev; while(x >> szam >> nev){... Ha az elválasztó jelek nem szeparálják egyértelműen a beolvasott értékeket (például egy név többtagú, azaz tartalmazhat szóközöket is), de feltehetjük, hogy a szöveges állomány soraiban az egyes adatokat rögzített pozíciókon helyeztük el, akkor a getline() utasítás segítségével egyszerre egy egész sort olvashatunk be egy sztringbe, amelyből ki tudjuk hasítani a megfelelő rész-sztringeket, hogy azokból aztán a kívánt értéket kinyerjük. Például, ha egy szöveg minden sorában az első négy pozíción egy négyjegyű szám, az azt követő húsz pozíción egy személy (szóközöket is tartalmazó) neve áll, akkor az alábbi kódot használhatjuk a sorszám-név párok felsorolására. 474

475 ifstream x("inp.txt"); string sor; for(getline(x,sor);!x.fail();getline(x,sor)){ int szam = atoi(sor.substr( 0, 4).c_str()); string nev = sor.substr( 4,20);... vagy ifstream x("inp.txt"); string sor; while(getline(x,sor)){ int szam = atoi(sor.substr( 0, 4).c_str()); string nev = sor.substr( 4,20);

476 30. Feladat: Könyvtár Egy szöveges állomány egy könyvtár adatait tartalmazza. Minden könyvről ismerjük az azonosítóját, a szerzőjét, címét, kiadóját, kiadásának évét, ISBN számát és azt, hogy hány példány van belőle jelenleg a könyvtárban. Egy könyv adatai a szöveges állomány egy sorát foglalja el szigorú pozicionálási szabályok mellett. Válogassuk ki a nulla példányszámú könyvek szerzőjét és címét. Specifikáció A feladat egy kiválogatás: egy szekvenciális inputfájlból kell az adott tulajdonságú elemeket kigyűjteni, és elhelyezni őket egy szekvenciális outputfájlban. A = ( x : SeqInFile(Könyv), y : SeqOutFile(Könyv2) ) Könyv = rec(azon:n, szerző:string, cím:string, kiadó:string, év:string, darab:n, isbn:string) Könyv2= rec(szerző:string, cím:string) Ef = ( x=x' ) Uf = ( y Absztrakt program dx x' dx. darab 0 dx. szerző, dx. cím A feladatot az összegzés programozási tételére vezetjük vissza, amely a szekvenciális inputfájl elemeinek felsorolására támaszkodik. ) y := <> sx,dx,x : read sx = norm dx.darab=0 476

477 y : write(<dx.szerző,dx.cím>) SKIP sx,dx,x : read Implementálás Az implementálásnál azt kell szem előtt tartani, hogy a szekvenciális inputfájl hátterében a feladatban megadott formájú szöveges állomány áll, amelyre definiálnunk kell az olvasás (read) műveletét. A szekvenciális outputfájl write műveletét egy szöveges állományba történő írással kell megvalósítani. Az implementációt kétféleképpen is elkészítjük. Először egy egyszerűbb változatot mutatunk, ahol a read és write műveleteket csak annyira különítjük el a kód többi részétől, hogy önálló alprogramokba ágyazzuk őket. Másodszor egy kicsit nagyobb feneket kerekítünk a megoldásnak, nevezetesen elkészítjük a feladat állapotterében szereplő két fájl típusát definiáló osztályokat, és ezen osztályoknak lesz read illetve write metódusa. Az első változat inkább egy procedurális szemléletű, a második egy objektum orientált szemléletű megoldást tükröz. Első megoldás szerkezete Az első megoldás szerkezete nagyon egyszerű. A main.cpp néhány egyszerű felhasználói típus (Könyv, Státusz) definiálása mellett három alprogramot tartalmaz. A main függvény gondoskodik a szöveges állományok szekvenciális input- és outputfájlként való megnyitásáról, tartalmazza az absztrakt program kódját, amely hívja a másik kettő alprogramot. main() Read() Write() ábra. Alprogramok hívási láncai 477

478 Első megoldás kódja A main függvényt megelőzi néhány fontos típusdefiníció. Nem definiáljuk külön a Könyv2 típust, mert annak mezői a Könyv típus mezőinek részét alkotják. struct Book{ int id; string author; string title; string publisher; string year; int piece; string isbn; ; enum Status{abnorm, norm; A main függvény megpróbálja megnyitni a bemeneti szöveges állományt és létrehozni a kimeneti szöveges állományt. Ha mindkét tevékenység sikerül, akkor kerül végrehajtásra az absztrakt programban rögzített kiválogatás. int main() { ifstream x("inp.txt"); if (x.fail() ) { cerr << "Nincs input file!\n"; 478

479 char ch; cin>>ch; exit(1); ofstream y("out.txt"); if (y.fail() ) { cerr << "Nincs output fájl!\n"; char ch; cin>>ch; exit(1); Book dx; Status sx; for(read(x,dx,sx); norm==sx; Read(x,dx,sx)) { if (0 == dx.count) { Write(y, dx.author, dx.title); return 0; A Read() művelet a klasszikus szekvenciális inputfájlból való olvasás, amely kihasználja, hogy a bemeneti szöveges állomány formája kötött, ezért egy teljes sor sikeres beolvasása után a sorból, mint sztringből ki lehet vágni az egyes részadatokat. void Read(ifstream &x, Book &dx, Status &sx) { string sor; 479

480 getline(x,sor,'\n'); if (!x.fail()) { sx = norm; dx.id = atoi(sor.substr( 0, 4).c_str()); dx.author = sor.substr( 5,14); dx.title = sor.substr(21,19); dx.publisher = sor.substr(42,14); dx.year = sor.substr(58, 4); dx.count = atoi(sor.substr(63, 3).c_str()); dx.isbn = sor.substr(67,14); else sx = abnorm; A Write() művelet egy új sort illeszt a kimeneti szöveges állományhoz. void Write(ofstream &y, const string &author, const string &title) { y << setw(14) << author << ' ' << setw(19) << title << endl; Második megoldás komponens szerkezete 480

481 Ebben a megvalósításban két külön osztály írja le a szekvenciális inputfájl (Stock) és a szekvenciális outputfájl (Result) típusát. Ezeket külön csomagokba helyezzük. main.cpp main() stock.h-stock.cpp class Stock Stock() Read() ~Stock() result.h-result.cpp class Result Result() Write() ~Result() ábra. Komponens szerkezet Második megoldás függvények hívási láncolata A vezérlést, azaz a komponensek egyes függvényeinek megfelelő sorrendben történő meghívását a main függvény biztosítja. Stock() Read() main() ~Stock() Result() Write() ~Result() ábra. Alprogramok hívási láncai 481

482 Második megoldás osztályai A Törzs típust leíró Stock osztály egy objektumát, egy törzsfájlt egy ifstream típusú objektummal reprezentáljuk. Ezt az osztály konstruktora nyitja meg és az inline definíciójú destruktora zárja be. Az osztály ezeken kívül még a Read() metódust tartalmazza: ez a soron következő könyv adatainak beolvasását végzi. Az osztály a Book és Status típusok definíciójával együtt a stock.h állományba tesszük. class Stock{ public: Stock(std::string fname); void Read(Book &df, Status &sf); private: std::ifstream f; ; A stock.cpp állományban helyeztük el a konstruktor és a Read() metódus implementációját. A konstruktor amennyiben nem kapja meg bemenetként megkérdezi a megnyitandó szöveges állomány nevét, majd megkísérli azt megnyitni. Stock::Stock(string fname = "") { if ( fname.size()<1 ) { cout << "Add meg a törzsfájl nevét:" ; cin >> fname; 482

483 f.open(fname.c_str()); if ( f.fail() ){ cerr << "Nincs törzs fájl" <<endl; char ch; cin>>ch; exit(2); A Read() metódus törzse szóról szóra megegyezik az első változat Read() műveletével. Csak a metódus paraméterezése tér el attól, és ennek megfelelően a hívásának a formája. void Stock::Read(Book &df, Status &sf) { string sor; getline(f, sor,'\n'); if (!f.fail()) { sf = norm; df.id = atoi(sor.substr( 0, 4).c_str()); 483

484 df.author = sor.substr( 5,14); df.title = sor.substr(21,19); df.publisher = sor.substr(42,14); df.year = sor.substr(58, 4); df.piece = atoi(sor.substr(63, 3).c_str()); df.isbn = sor.substr(67,14); else sf = abnorm; Az Eredm típust leíró Result osztály a result.h állományba kerül. class Result{ public: Result(std::string fname); void Write(const std::string &author, const std::string &title); private: std::ofstream f; ; Az result.cpp állományban helyeztük el a konstruktor és a Write() metódus implementációját. A konstruktor szinte szó szerinti mása a Stock osztály konstruktorának. 484

485 Result::Result(string fname = "") { if ( fname.size()<1 ) { cout << "Add meg a törzsfájl nevét:" ; cin >> fname; 485

486 f.open(fname.c_str()); if ( f.fail() ){ cerr << "Nincs törzs fájl" <<endl; char ch; cin>>ch; exit(2); A Write() metódus lényegében megegyezik az első változat Write() műveletével. void Result::Write(const string &author, const string &title) { f << setw(14) << author << ' ' << setw(19) << title << endl; Második megoldás főprogramja A fent bevezetett osztályoknak köszönhetően a main függvény kizárólag az absztrakt program kódját tartalmazza, ezáltal végletesen mellőz minden, a konkrét implementációval kapcsolatos részletet. Ezek ugyanis az osztályokban vannak elrejtve. int main() 486

487 { Stock x("inp.txt"); Result y("out.txt"); Book dx; Status sx; for(x.read(dx,sx); norm==sx; x.read(dx,sx)) { if (0==dx.piece) y.write(dx.author, dx.title); return 0; Tesztelés A két változat fekete doboz tesztesetei megegyeznek: Ennek alapját a kiválogatás (összegzés) adja, amelyiknél elsősorban az intervallum tesztre (amit most a szekvenciális inputfájl vált ki) kell figyelni. 1. Üres törzsfájl esete. 2. Nem üres, csupa nulla darabszámú könyv a törzsfájlban. 3. Ne üres, csupa nem-nulla darabszámú könyv a törzsfájlban. 4. Általános eset nem üres törzsfájlra. 5. Az első és az utolsó könyv darabszáma nulla a törzsfájlban. A fehér doboz tesztelése sem tér el egymástól a két verziónak. Az, amit ennek keretében külön meg kell vizsgálni, az a fájlnyitásoknak, illetve az írás/olvasásnak a megfelelő működése és hibakezelése. Ebben a tekintetben a második változat annyival tud többet, hogy ha a fájl nevét nem adjuk meg, 487

488 akkor megkérdezi azt. Az osztályoknak a modul tesztjeit nem részletezzük, mert azokból nem adódnak újabb tesztesetek. A második változatban elvileg komponens tesztet is kell csinálni. Tekintettel arra, hogy az osztályoknak lényegében egy-egy metódusa van, amelyek egyszerű beolvasást illetve kiírást végeznek, ezek vizsgálatához a korábbi tesztesetek elegendőek. Variációs teszt nem kell. 488

489 Teljes program Az első változat teljes kódja: #include <fstream> #include <iostream> #include <iomanip> #include <string> using namespace std; struct Book{ int id; string author; string title; string publisher; string year; int piece; string isbn; ; enum Status{abnorm, norm; void Read(ifstream &x, Book &dx, Status &sx); void Write(ofstream &x, const string &author, const string &title); 489

490 int main() { ifstream x("inp.txt"); if (x.fail() ) { cerr << "Nincs input file!\n"; char ch; cin>>ch; return 1; ofstream y("out.txt"); if (y.fail() ) { cerr << " Nincs output fájl!\n"; char ch; cin>>ch; return 1; Book dx; Status sx; for(read(x,dx,sx); norm==sx; Read(x,dx,sx)) { if (0 == dx.piece) { Write(y, dx.author, dx.title); return 0; void Read(ifstream &x, Book &dx, Status &sx) { 490

491 string sor; getline(x,sor,'\n'); if (!x.fail()) { sx = norm; dx.id = atoi(sor.substr( 0, 4).c_str()); dx.author = sor.substr( 5,14); dx.title = sor.substr(21,19); dx.publisher = sor.substr(42,14); dx.year = sor.substr(58, 4); dx.piece = atoi(sor.substr(63, 3).c_str()); dx.isbn = sor.substr(67,14); else sx = abnorm; void Write(ofstream &y, const string &author, const string &title) { y << setw(14) << author<< ' ' << setw(19) << title << endl; 491

492 A második változat teljes kódja: main.cpp: #include <fstream> #include <string> #include "stock.h" #include "result.h" using namespace std; int main() { Stock x("inp.txt"); Result y("out.txt"); Book dx; Status sx; for(x.read(dx,sx); norm==sx; x.read(dx,sx)) { if (0 == dx.piece) y.write(dx.author, dx.title); return 0; stock.h: #ifndef _STOCK_ #define _STOCK_ #include <fstream> 492

493 #include <string> struct Book { int id; std::string author; std::string title; std::string publisher; std::string year; int piece; std::string isbn; ; enum Status {abnorm,norm; class Stock{ public: Stock(std::string fname); void Read(Book &df, Status &sf); private: std::ifstream f; ; #endif stock.cpp: #include "stock.h" 493

494 #include <iostream> #include <cstdlib> using namespace std; Stock::Stock(string fname = "") { if ( fname.size()<1 ) { cout << "Add meg a törzsfájl nevét:" ; cin >> fname; f.open(fname.c_str()); if ( f.fail() ){ cerr << "Nincs törzs fájl" <<endl; char ch; cin>>ch; exit(2); void Stock::Read(Book &df, Status &sf) { string sor; getline(f, sor,'\n'); if (!f.fail()) { 494

495 sf = norm; df.id = atoi(sor.substr( 0, 4).c_str()); df.author = sor.substr( 5,14); df.title = sor.substr(21,19); df.publisher = sor.substr(42,14); df.year = sor.substr(58, 4); df.piece = atoi(sor.substr(63, 3).c_str()); df.isbn = sor.substr(67,14); else sf = abnorm; 495

496 result.h: #ifndef _RESULT_ #define _RESULT_ #include <fstream> #include <string> class Result{ public: Result(std::string fname); void Write(const std::string &author, const std::string &title); private: std::ofstream f; ; #endif result.cpp: #include "result.h" #include <iostream> #include <cstdlib> #include <iomanip> using namespace std; 496

497 Result::Result(string fname = "") { if ( fname.size()<1 ) { cout << "Add meg a törzsfájl nevét:" ; cin >> fname; f.open(fname.c_str()); if ( f.fail() ){ cerr << "Nincs törzs fájl" <<endl; char ch; cin>>ch; exit(2); void Result::Write(const string &author, const string &title) { f << setw(14) << author << ' ' << setw(19) << title << endl; 497

498 31. Feladat: Havi átlag-hőmérséklet Adott egy szöveges állományban egy adott időszak napi átlaghőmérsékleteinek sorozata. Az állomány minden sora egy év-hó-nap (eehhnn) formátumban megadott dátumot tartalmaz, amelyet egy szóköz, majd egy hőmérsékleti érték követ. Az állomány dátum szerint növekedően rendezett. Hány olyan egymást követő hónap-pár van, ahol a havi átlaghőmérséklet megegyezik? Specifikáció A feladat egy számlálás, amelyet azonban nem az állományban megadott dátum-hőmérséklet párok sorozata felett kell közvetlenül értelmezni, hanem azon átlaghőmérséklet-párok sorozata felett, amelyek két szomszédos hónap havi átlaghőmérsékleteiből állnak. Bevezetjük tehát e szám-párokat szolgáltató absztrakt felsorolót (enor(pár)). A = ( t : enor(pár), darab : N ) Pár = rec(akt:r, elő:r) Ef = ( t=t' ) Uf = ( darab 1 ) aktpár t' aktpár. akt aktpárelő. A t felsoroló megvalósításához egy másik absztrakt felsorolóra is szükség van, amelyik rendre elő tudja állítani (fel tudja sorolni) az egyes hónapok havi átlaghőmérsékleteit. Ez a valós számokat felsoroló objektum (x:enor(r)) a t felsoroló reprezentációjának része lesz, kiegészítve a t felsorolásának végét jelző logikai értékkel (tvége: ) és a legutoljára előállított havi átlaghőmérséklet párral (aktpár:pár). A t felsoroló műveleteinek megvalósítása az x felsoroló műveleteire építve készült: t.first() ~ x.first() ha x.end() akkor aktpár.előző:=x.current() x.next(); tvége:=x.end() ha x.end() akkor aktpár.akt:=x.current() 498

499 t.next() ~ x.next(); tvége:=x.end() ha x.end() akkor aktpár.előző:=aktpár.akt aktpár.akt:=x.current() t.current() ~ aktpár t.end() ~ tvége Az x felsoroló megvalósításához a szöveges állomány dátumhőmérséklet párjait kell felsorolnunk. Ez tehát egy harmadik felsoroló, amely a szöveges állomány sorait tudja bejárni. Ehhez a szekvenciális inputfájl nevezetes felsorolója (f:seqinfile(nap), Nap= rec(hó:n, hő:r)) kell, azaz elég azt a read műveletet definiálni, amely egy nap mérési adatát olvassa ki az aktuális sorból: a dátumot (ebből nekünk csak a hónap sorszáma kell) és a napi átlaghőmérsékletet. Az x reprezentációja tartalmazza a szöveges állományra épülő szekvenciális inputfájlt, az x felsorolásának végét jelző logikai értéket (xvége: ) és a legutoljára vizsgált hónap havi átlaghőmérsékletét (havi:r). Ezeken kívül a reprezentáció kiegészül a szekvenciális inputfájl olvasó műveletének (read) segédadataival (st:státusz, nap:nap). Az x felsoroló műveleteit az alábbiak szerint implementáljuk (az alkalmazott jelöléseket az első kötetben vezettük be): x.first() ~ st,nap,f:read x.next() x.next() ~ xvége:= st=abnorm ha st=norm akkor hó:=nap.hó össz, st, nap, f: nap. hó nap nap. hó db, st, nap, f: 1 nap hó nap. hő ( nap, f ) hó ( nap, f ) 499

500 havi:=össz/db x.current() ~ havi x.end() ~ xvége Absztrakt program A feladatot megoldása több, egymásra épülő rétegből tevődik össze. Legfelül egy számlálás, amelyik a t felsoroló működésére épül: darab := 0 t.first() t.end() t.current().akt=t.current().elő darab:=darab+1 SKIP t.next() A t felsoroló típusa alkotja a következő szintet, amelyhez viszont az x felsoroló típusa szükséges, amely a harmadik szint, ahol az adatfolyam objektumra lesz szükség, amely segítségével a szöveges állomány olvasható. Itt valójában átugrunk egy szintet, mert nem valósítjuk meg önálló osztályként annak a szekvenciális inputfájlnak a típusát, amellyel a szöveges állomány sorait tudjuk kiolvasni. Ezért a szekvenciális inputfájlnak a read művelete az x felsoroló típusának szintjén kerül majd megadásra. A t felsoroló műveleteinek implementálása nem igényel ciklust, ezért a művelet-implementációk algoritmusát nem részletezzük. Az x felsoroló műveletei közül csak a x.next() műveletnek érdemes külön felírni az absztrakt programját. Ez egy elágazás, amelyen belül két feltétel fennállásáig (amíg ugyanazon hónap hőmérsékleteit vizsgáljuk) tartó összegzés szerepel: egyik összeadja a napi átlaghőmérsékleteket, a másik megszámolja, hány nap van a hónapban. Mindkét összegzés ugyanazon a két ponton tér el egy szekvenciális inputfájl elemeinek nevezetes felsorolására épülő összegzéstől. 500

501 Egyrészt az End() műveletük már a fájlvége előtt is igaz lehet, ha hónap végére értünk, tehát a ciklusfeltétel kiegészül az aktuális hónap figyelésével. Másrészt nem igényelnek előre olvasást, mert a hónap legelső napját vagy az x.first(), vagy az előző x.next() végén már beolvastuk. A két összegzést lévén azonos szerkezetűek egyetlen ciklusba vonjuk össze. xvége := st=abnorm st=norm hó, össz, db:=nap.hó, 0, 0 st=norm hó=nap.hó SKIP össz, db := össz+nap.hő, db+1 st, nap, f : read havi:=össz/db Implementálás Az implementációban önálló osztályként fogalmazzuk meg az enor(pár) és az enor(r) típusát, a szöveges állománybeli napi adatok felsorolásához pedig egy Read() alprogramot fogunk használni. Az osztályok publikus elemei a bejáró műveletek lesznek. Figyelnünk kell arra, hogy a tervezésnél előállt modulok megfelelő módon hivatkozzanak egymásra, és egy-egy nyelvi elem láthatósága biztosítva legyen ott, ahol használni akarjuk. Ezen túlmenően néhány egyszerű átalakítás is végzünk. Ilyen például az, hogy egy double-ként definiált valós változónak kezdőértéke nem 0 hanem 0.0 lesz, vagy, hogy segédváltozók bevezetésével csökkentjük egy-egy metódus ismételt meghívásainak számát. A program komponens szerkezete A main.cpp main függvénye tartalmazza az absztrakt megoldást alkotó számlálást. Ez a program a Pair_Enor (enor(pár)) osztályt használja, 501

502 ezért a main.cpp állományba be kell inklúdolni a pair_enor.h állományt. A Pair_Enor osztálynak a Month_Average_Enor (enor(r)) osztályt kell elérnie, ezért a pair_enor.h állományba a month_average_enor.h állományt kell beinklúdolni. A szöveges állomány olvasását biztosító Read() függvényt a Month_Average_Enor osztály privát metódusaként adjuk meg, hiszen ezt csak az itteni First() és Next() művelet használja. Szükség lesz a Month_Average_Enor osztályban egy publikus Open() műveletre, amelyik olvasásra megnyitja a szöveges állományt. main.cpp pair_enor.hpair_enor.cpp average_enor.haverage_enor.cpp main() class Pair_Enor First() Next() End() Current() class Average_Enor First() Next() End() Current() Open() Read() ábra. Komponens szerkezet Főprogram kódolása A tervezés során bevezetett absztrakciós szinteknek köszönhetően a main függvény a lehető legegyszerűbb lett, a legfelső szint számlálását tartalmazza: int main() { 502

503 Pair_Enor t("input.txt"); int count = 0; for(t.first();!t.end(); t.next()){ if(t.current().curr == t.current().prev) ++count; cout << "Azonos hőmérséklet párok száma: " << count; return 0; A main függvény hívja a Pair_Enor osztály metódusait. A konstruktornak a megnyitandó szöveges állomány nevét adja át. Pair_Enor() First() main() Next() End() Current() ábra. Alprogramok hívási láncai Pair_Enor osztály 503

504 A tervezésben enor(pár)-ként definiált Pair_Enor osztály a t felsoroló típusát határozza meg. Az osztály kódja külön fej- és forrásállományba kerül. Az osztály definíciója előtt definiálni kell a Pair (Pár) típust. struct Pair{ double prev; double curr; ; class Pair_Enor{ private: Month_Average_Enor x; bool end; Pair current; ; public: Pair_Enor(const std::string &str) { x.open(str); void First(); void Next(); Pair Current() const { return current; bool End() const { return end; A Pair_Enor osztály definíciója inline módon tartalmazza a Current(), az End() és a konstruktor implementálását. 504

505 A tervezésnél az osztály konstruktoráról nem esett szó. Ez egy sztringet (szöveges állomány neve) kap bemenetként, és ezzel hívja meg a Month_Average_Enor osztály Open() függvényét, amely a szöveges állományt nyitja majd meg. Pair_Enor() Open() ábra. Alprogramok hívási láncai A First() és a Next() műveletek kódjában egy apró változtatás a tervhez képest például az, hogy a második és harmadik if utasítás feltétele az end segédváltozóra hivatkozik, nem hívja meg újra és újra az x.end() metódust. A Pair_Enor osztály First() és a Next() metódusai a Month_Average_Enor osztály bejáró metódusait hívják. First() First() Next() End() Current() Next() Next() End() Current() ábra. Alprogramok hívási láncai 505

506 void Pair_Enor::First() { x.first(); if(!x.end())current.prev = x.current(); x.next(); end = x.end(); if(!end)current.curr = x.current(); void Pair_Enor::Next() { x.next(); end = x.end(); if (!end) { current.prev = current.curr; current.curr = x.current(); 506

507 Month_Average_Enor osztály A tervezésben enor(r)-ként definiált osztály az x felsoroló típusát határozza meg. Az osztály kódja külön fej- és forrásállományba kerül. Itt implementáljuk a szöveges állományból történő Read() olvasó műveletet is, amely egy Day (Nap) típusú elemet olvas az állomány egy sorából és beállítja az olvasás státuszát is. Meg kell tehát adnunk a Status és a Day típust. enum Status { abnorm, norm ; struct Day{ int month; double term; ; Jól láthatóak az osztály tervezésekor már említett privát adattagok: az adatfolyam (f), az olvasás két segédadata (day, st), az olvasás művelete (Read()), a havi átlaghőmérsékletek felsorolásának végét jelző logikai változó (end), valamint az utoljára felsorolt hónap átlag hőmérséklete (avrterm). A láthatósági szabályok miatt nem kell nevében megkülönböztetni az itteni end tagot a Pair_Enor end tagjától. (A tervezésnél ezekre még külön elnevezést használtunk.) class Month_Average_Enor{ private: std::ifstream f; Day day; Status st; 507

508 bool end; double avrterm; void Read(); public: void Open(const std::string &str); void First() { Read(); Next(); void Next(); double Current() const { return avrterm; bool End() const { return end; ; Az osztály definíciója inline módon tartalmazza a destruktor, a First(), a Current() és az End() implementálását. Ezek, csakúgy, mint a Next() művelet kódja, megfelelnek a tervezésnél megadott implementációnak. void Month_Average_Enor::Next() { end = abnorm == st; if(!end){ avrterm = 0.0; int c = 0; for(int month = day.month; norm == st && month == day.month; Read() ){ 508

509 avrterm += day.term; ++c; avrterm /= c; Az Open() bemenetként a megnyitandó szöveges állomány nevét kapja meg, és ha ez létezik, akkor egy ifstream adatfolyamot nyit erre az állományra, amelyet a Month_Average_Enor destruktora zárja be. void Month_Average_Enor::Open(const string &str) { f.open(str.c_str()); if(f.fail()){ cout << "Inputfajl hiba!\n"; exit(1); A Read() művelet az állomány soron következő sorát beolvasva kinyeri abból az adott nap hónapjának számát és a napi átlaghőmérsékletet. Ennek a műveletnek a paraméterei a Month_Average_Enor privát adattagjai, nevezetesen a f adatcsatorna, a day adat és az st státusz. void Month_Average_Enor::Read() 509

510 { string date; f >> date; if(!f.fail()){ st = norm; day.month = atoi(date.substr(2,2).c_str()); f >> day.term; else st = abnorm; Tesztelés Fekete doboz tesztesetek: (számlálás és összegzések) 1. Üres állomány. 2. Egyetlen nap adatait (egy sort) tartalmazó állomány. 3. Egyetlen hónap adatait tartalmazó állomány. 4. Több, nem azonos átlaghőmérsékletű hónap adatait tartalmazó állomány. 5. Olyan állomány, ahol csak az első két hónap átlaghőmérséklete azonos. 6. Olyan állomány, ahol csak az utolsó két hónap átlaghőmérséklete azonos. 7. Általános eset, ahol több egymás utáni hónap-pár átlaghőmérséklete is azonos. 8. Általános eset, ahol minden hónap átlaghőmérséklete azonos. Fehér doboz tesztesetek a fentieken kívül: 510

511 1. Nem létező állomány név. Komponens tesztet nem kell külön csinálni. Egyrészt itt egy felsorolót megvalósító osztállyal van dolgunk, amelynél a metódusok variációs tesztje elhagyható, másrészt a metódusok egyszerű beolvasást végeznek, amelyet a fenti tesztesetek vizsgálnak. 511

512 Teljes program main.cpp: #include <iostream> #include "pair_enor.h" using namespace std; int main() { Pair_Enor t("input.txt"); int count = 0; for(t.first();!t.end(); t.next()){ if(t.current().curr == t.current().prev) ++count; cout << "Azonos hőmérséklet-párok száma: " << count; return 0; 512

513 513

514 pair_enor.h: #ifndef PAIR_ENOR_H #define PAIR_ENOR_H #include <string> #include "month_average_enor.h" struct Pair{ double prev; double curr; ; class Pair_Enor{ private: Month_Average_Enor x; bool end; Pair current; public: Pair_Enor(const std::string &str) { x.open(str); void First(); void Next(); Pair Current() const { return current; bool End() const { return end; ; #endif 514

515 pair_enor.cpp: #include "pair_enor.h" void Pair_Enor::First() { x.first(); if(!x.end())current.prev = x.current(); x.next(); end = x.end(); if(!end)current.curr = x.current(); void Pair_Enor::Next() { x.next(); end = x.end(); if (!end) { current.prev = current.curr; current.curr = x.current(); 515

516 month_average_enor.h: #ifndef MONTH_AVERAGE_ENOR_H #define MONTH_AVERAGE_ENOR_H #include <fstream> #include <string> enum Status { abnorm, norm ; struct Day{ int month; double term; ; class Month_Average_Enor{ private: std::ifstream f; Day day; Status st; bool end; double avrterm; void Read(); public: void Open(const std::string &str); void First() { Read(); Next(); void Next(); 516

517 double Current() const { return avrterm; bool End() const { return end; ; #endif month_average_enor.cpp: #include "month_average_enor.h" #include <iostream> #include <cstdlib> using namespace std; void Month_Average_Enor::Open(const string &str) { f.open(str.c_str()); if(f.fail()){ cout << "Inputfájl hiba!\n"; exit(1); 517

518 void Month_Average_Enor::Next() { end = abnorm == st; if(!end){ avrterm = 0.0; int db = 0; for(int month = day.month; norm == st && month == day.month; Read() ){ avrterm += day.term; ++db; avrterm /= db; void Month_Average_Enor::Read() { string date; f >> date; if(!f.fail()){ st = norm; day.month = atoi(date.substr(2,2).c_str()); f >> day.term; 518

519 else st = abnorm; 519

520 32. Feladat: Bekezdések Egy szöveges állományban bekezdésekre tördelt szöveg található. Egy bekezdés egy vagy több nem üres sorból áll. A bekezdéseket üres sorok vagy az állomány eleje illetve vége határolja. Melyik a leggazdagabb bekezdés, azaz hányadik az a legalább három soros bekezdés, amelyik tartalmazza az alma szót önmagában vagy valamilyen szóösszetételben és az ilyen bekezdések közül nála a legnagyobb a szavak számának és a sorok számának hányadosa? A szövegben egyik szó sincs több sorra tördelve, a szavakat szóközök, tabulátor-jelek és sorvége-jelek (akár egymás után több is) választhatja el egymástól. Specifikáció A feladat megoldása egy feltételes maximumkeresés, amelyik azt a bekezdést keresi meg az alma szót tartalmazó bekezdések között, ahol a legnagyobb a szavak számának és a sorok számának hányadosa. Ehhez egy olyan absztrakt felsorolóra van szükségünk, amely az egyes bekezdések statisztikáit képes megadni: a bekezdés sorszámát, sorainak számát, szavainak számát, valamint, hogy szerepel-e benne az alma szó. A = ( t : enor(bekezdés), l :, max : R, ind : N ) Bekezdés = rec(sorsz:n, szó:n, sor:n, alma: ) Ef = ( t=t' ) Uf = ( l, max, elem max e. szó / e. sor e t' e. alma e. sor 3 l ind = elem.sorsz ) A t felsoroló First() illetve Next() művelete egy bekezdésnyit olvas a szövegből. Ehhez számon tartják, hogy hányadik bekezdésnél tartunk, először átlépik az üres sorokat, megszámolják a szavak és a sorok számát és figyelik az alma szó előfordulását. Mindehhez a szöveges állomány sorait kell tudnunk bejárni. Ehhez a szöveges állományt szekvenciális inputfájlként (f:seqinfile(sztring)) kell kezelni, így az állományból könnyű egy teljes sort 520

521 beolvasni. El kell készítenünk egy read műveletet, amely beállítja az olvasás státuszát és a beolvasott sort egy sztring típusú változóba helyezi (st:státusz, sor:sztring). Ezen kívül tárolnunk kell a legutoljára beolvasott bekezdés statisztikáját (akt:bekezdés), és azt a logikai értéket (vége: ), amely jelzi, ha már nincs több bekezdés. A t felsoroló műveleteit az alábbiak szerint implementáljuk: t.first() ~ akt.sorsz:=0 t.current() ~ akt st, sor, f:read t.next() t.end() ~ vége t.next() ~ st, sor, f select üres( sor) sor ( sor, f ) vége:= st=abnorm ha st=norm akkor akt.sorsz:= akt.sorsz+1 akt. alma, st, sor, f: sor üres( sor) ( sor, f ) üres( sor) akt. sor, st, sor, f: 1 akt. szó, st, sor, f: sor ( sor, f ) üres( sor) sor ( sor, f ) " alma" sor sorbeli szavak száma 521

522 Absztrakt program A feladatot megoldó feltételes maximumkeresés: l:= hamis; t.first() t.end() bek:=t.current() (bek.alma bek.sor 3 ) bek.alma bek.sor 3 l bek.alma bek.sor 3 l SKIP bek.szó/bek.sor>max l, max, ind := max, ind:= bek.szó/bek.sor, bek.sorsz t.next() SKIP igaz, bek.szó/bek.sor, bek.sorsz Egyedül a Next() metódus megvalósítása tartalmaz ciklusokat. st=norm üres(sor) st, sor, f : read vége := st=abnorm st=norm akt.sorsz, akt.alma, akt.sor, akt.sor := akt.sorsz+1, hamis, 0, 0 st=norm üres(sor) SKIP akt.alma := akt.alma alma sor akt.sor:=akt.sor+1 akt.szó := akt.szó+ 522

523 (sor-beli szavak száma) st, sor, f : read Az akt.alma, akt.sor és akt.szó értékét meghatározó összegzések ciklusait egy ciklusba vonjuk össze. Az üres(sor), alma sor és a sor-beli szavak száma kifejezéseket majd az implementációban pontosítjuk. Implementálás A t felsoroló típusát egy osztály segítségével célszerű megadni. Az osztályok privát tagjait a felsorolót reprezentáló elemek alkotják, valamint a szöveges állomány egy sorát kiolvasó read művelet. Ezen a túlmenően az implementálásra már csak annyi maradt, hogy megtaláljuk a helyes kódot az olyan részletek leírására, mint az üres(sor), alma sor és a sor-beli szavak száma. A program komponens szerkezete A feltételes maximumkeresést a main.cpp forrásállomány main függvényben helyezzük el a kiírással együtt, a felsoroló típusát leíró osztályt pedig külön fej- és forrásállományban (enor.h, enor.cpp) adjuk meg. A main.cpp állománynak be kell inklúdolnia az enor.h állományt. A program komponens szerkezete meglehetősen egyszerű. 523

524 main.cpp enor.h-enor.cpp main() class Enor Enor() First() Next() End() Current() ~Enor() ábra. Komponens szerkezet Főprogram kódolása int main() { Enor t("input.txt"); int ind; double max; bool l = false; for(t.first();!t.end(); t.next()){ Statistic bek = t.current(); if(!(bek.apple && bek.line>=3)) continue; double rate = (double)bek.word/(double)bek.line; if(l && rate>max) { 524

525 max = rate; ind = bek.no; else { l = true; max = rate; ind = bek.no; if (l) cout << "A \"leggazdagabb\" bekezdés a " << ind << "-dik\n" << "arány: " << max << endl; else cout << "Nincs \"gazdag\" bekezdés!\n"; return 0; A main függvény hívja az Enor osztály metódusait. A konstruktornak a megnyitandó szöveges állomány nevét adja át. A függvény befejeződésekor a destruktor is meghívódik. 525

526 Enor() First() main() Next() End() Current Current() ábra. Alprogramok hívási láncai Enor osztály A bekezdések bejárásának típusát leíró osztály előtt definiálnunk kell a bekezdés statisztikáját megadó Statistic típust (a tervezésben ez Bekezdés néven szerepelt). Mivel itt implementáljuk a szöveges állományból történő olvasó műveletet is, ezért be kell vezetni a Status típust. enum Status { abnorm, norm ; struct Statistic{ bool apple; int word; int line; int no; ; 526

527 Az Enor osztály privát tagjai között találjuk a felsorolót reprezentáló ifstream típusú f objektumot (szekvenciális inputfájl), az aktuális bekezdés statisztikáját tartalmazó current változót (tervezésnél akt néven szerepelt), és a felsorolás végét jelző end flag-et (vége). Ezek kiegészülnek a szöveges állományból való olvasás adataival: az aktuális sort tartalmazó line nevű istringstream taggal (sor) és az utolsó olvasás státuszával (st). Privát elem lesz a Read() olvasó metódus is. class Enor{ private: std::ifstream f; std::istringstream line; Status st; bool end; Statistic current; void Read(); Az osztály definíciója inline módon tartalmazza a destruktor, a First(), a Current() és az End() implementálását. Ezek, csakúgy, mint a Next() művelet kódja, megfelelnek a tervezésnél megadott implementációnak. public: Enor(const std::string &str); void First() { current.no = 0; Read(); Next(); void Next(); 527

528 Statistic Current() const { return current; bool End() const { return end; ; A Next() metódus megvalósításánál meg kell vizsgálni, hogy a C++ nyelv milyen lehetőségeket kínál az üres(sor), az alma szavak száma kifejezések implementálására. sor és a sor-beli A sor most egy line nevű változóban van. Ha a line egy istringstream típusú objektum, akkor a sorbeli szavakat a >> operátorral egyenként ki tudjuk olvasni a line>>w utasítással, ahol w egy sztring típusú változó. A line.fail()jelzi majd, ha a sor végére értünk. Ez egyszerűvé teszi a sorbeli szavak megszámolását. Ráadásul, ha már kezünkben van sztringként egy szó, akkor abban a find() metódussal kereshetünk alma szót. A w.find("alma") egy különleges string::npos értéket ad vissza, ha nem szerepel az alma szó a w sztringben. Az üres(sor) feltétel akkor teljesül, ha a sor, mint sztring, üres, azaz nulla hosszú. A line-beli sor méretére line.srt().size() kifejezéssel hivatkozhatunk. void Enor::Next() { for(;norm==st && line.str().size()==0; Read()); end = abnorm == st; if(!end){ ++current.no; current.word = current.line = 0; current.apple = false; 528

529 for(;norm==st && line.str().size()!= 0; Read()){ ++current.line; string w; for(line>>w;!line.fail(); line>>w){ ++current.word; current.apple = current.apple w.find("alma")!=string::npos; Az Enor konstruktora nyit egy ifstream csatornát a paraméterként megadott nevű szöveges állományra. void Enor::Enor(const string &str) { f.open(str.c_str()); if(f.fail()){ cout << "Inputfájl hiba!\n"; exit(1); 529

530 A Read() művelet az állomány soron következő sorát olvassa be, mint egy sztringet, és ezt alakítja át istringstream típusú adattá. Erre szolgál a line.clear() és a line.str() metódus. void Enor::Read() { string str; getline(f,str,'\n'); if(!f.fail()){ st = norm; line.clear(); line.str(str); else st = abnorm; Tesztelés Fekete doboz tesztesetek: (Feltételes maximum keresés, azon belül kiválasztás és összegzések.) 1. Üres állomány 2. Egyetlen legalább három soros bekezdés, ahol nincs alma szó. 3. Egyetlen egy soros bekezdés, amely tartalmaz alma szót. 4. Egyetlen két soros bekezdés (előtte és utána több üres sorral), amelynek minden sora tartalmaz alma szót. 530

531 5. Egyetlen legalább három soros bekezdés (előtte és utána több üres sorral), amelynek az első sorának első szava alma. 6. Egyetlen legalább három soros bekezdés (előtte és utána több üres sorral), amelynek az első sorának utolsó szava alma. 7. Egyetlen legalább három soros bekezdés (előtte és utána több üres sorral), amely utolsó sorának első szava alma. 8. Egyetlen legalább három soros bekezdés (előtte és utána több üres sorral), amely utolsó sorának utolsó szava alma. 9. Egyetlen legalább három soros bekezdés (előtte és utána több üres sorral), amely középső sorának első szava alma. 10. Egyetlen legalább három soros bekezdés (előtte és utána több üres sorral), amely középső sorának utolsó szava alma. 11. Több legalább három soros, alma szavas bekezdés, a bekezdések előtt, között, utána több üres sorral, és a legelsőben a legnagyobb a szó/sor arány. 12. Több legalább három soros, alma szavas bekezdés, a bekezdések előtt, között, utána több üres sorral, és a legutolsóban a legnagyobb a szó/sor arány. 13. Több legalább három soros, alma szavas bekezdés, a bekezdések előtt, között, utána több üres sorral, és egy középsőben a legnagyobb a szó/sor arány. 14. Több legalább három soros, alma szavas bekezdés, a bekezdések előtt, között, utána több üres sorral, és minden bekezdés egyformán gazdag. 15. Általános eset több bekezdéssel, a bekezdések előtt, között, utána több üres sorral. Fehér doboz tesztesetek a fentieken kívül: 1. Nem létező állomány név. Komponens tesztet nem kell külön csinálni. Egyrészt itt egy felsorolót megvalósító osztállyal van dolgunk, amelynél a metódusok variációs tesztje elhagyható, másrészt a metódusok a Next() kivételével egyszerű beolvasást 531

532 végeznek, amelyet a fenti tesztesetek vizsgálnak. A Next() metódus kódja programozási tételre támaszkodik, amelyet a fekete doboz tesztesetek érintettek. 532

533 Teljes program main.cpp: #include <iostream> #include "enor.h" using namespace std; int main() { Enor t("input.txt"); int ind; double max; bool l = false; for(t.first();!t.end(); t.next()){ Statistic bek = t.current(); if(!(bek.apple && bek.line>=3)) continue; double rate = (double)bek.word/(double)bek.line; if(l && rate>max) { max = rate; ind = bek.no; else { l = true; 533

534 max = rate; ind = bek.no; if (l) cout << "A \"leggazdagabb\" bekezdés a " << ind << "-dik\n" << "arány: " << max << endl; else cout << "Nincs \"gazdag\" bekezdés!\n"; return 0; 534

535 enor.h: #ifndef ENOR_H #define ENOR_H #include <fstream> #include <string> #include <sstream> enum Status { abnorm, norm ; struct Statistic{ bool apple; int word; int line; int no; ; class Enor{ private: std::ifstream f; std::stringstream line; Status st; bool end; Statistic current; void Read(); 535

536 public: Enor(const std::string &str); void First() { current.no = 0; Read(); Next(); void Next(); Statistic Current() const { return current; bool End() const { return end; ; #endif 536

537 enor.cpp: #include "enor.h" #include <iostream> #include <cstdlib> using namespace std; Enor::Enor(const string &str) { f.open(str.c_str()); if(f.fail()){ cout << "Inputfájl hiba!\n"; exit(1); void Enor::Next() { for(;norm==st && line.str().size()==0; Read()); end = abnorm == st; if(!end){ ++current.no; current.word = current.line = 0; current.apple = false; for(;norm==st && line.str().size()!= 0; 537

538 Read()){ ++current.line; string w; for(line>>w;!line.fail(); line>>w){ ++current.word; current.apple = current.apple w.find("alma")!=string::npos; void Enor::Read() { string str; getline(f,str,'\n'); if(!f.fail()){ st = norm; line.clear(); line.str(str); else st = abnorm; 538

539 C++ kislexikon szöveges állomány karakterenkénti olvasása ifstream x("inp.txt"); char ch; for(x.get(ch);!x.fail(); x.get(ch)){... ifstream x("inp.txt"); char ch; x.unsetf(ios::skipws); for(x >> ch;!x.fail(); x >> ch){... szöveges állomány számainak olvasása ifstream x("inp.txt"); int n, db = 0; for(x >> n;!x.fail(); x >> n){... szöveges állományból szám-név párok olvasása ifstream x("inp.txt"); int szam; string nev; for(x>>szam>>nev;!x.fail(); x>>szam >>nev){ 539

540 ... szöveges állomány soronkénti olvasása, a sorokból szám és név kinyerése ifstream x("inp.txt"); string sor; for( getline(x,sor,'\n');!x.fail(); getline(x,sor,'\n')){ int szam = atoi(sor.substr( 0, 4).c_str()); string nev = sor.substr( 4,20);

541 13. Dinamikus szerkezetű típusok osztályai Egy típus adatszerkezetén a típus egy értékét reprezentáló elemek egymáshoz való viszonyát, rákövetkezési kapcsolatainak rendszerét értjük. Sok típusnak statikus (állandó) adatszerkezete van, azaz a típusértékek mindegyikét azonos számú elem helyettesíti és az elemek közötti kapcsolatok is rögzítettek. Ennél fogva két típusérték csak az elemeik értékeiben különbözhetnek. Tipikusan ilyen a tömb vagy a rekord típus. Amikor azonban egy típusértéket reprezentáló elemek száma és/vagy azok kapcsolati rendszere változhat, akkor dinamikus (változó) adatszerkezetről beszélhetünk. Gondoljunk például egy karakterlánc típusra, amelynek értékei különböző hosszú (eltérő számú karaktert tartalmazó) sorozatok, és ahol megengedett egy ilyen sorozat egy részének kivágása vagy az abba való betoldás, ami megváltoztatja a karakterláncon belül a karakterek közötti rákövetkezési kapcsolatokat. Amikor egy összetett szerkezetű típusnak egy adott programozási nyelven való megvalósítására kerül sor, akkor döntenünk kell többek között arról, hogy egy típusértéket hogyan helyezzünk el, hogyan reprezentáljunk a memóriában. Ennek egyik módja az, amikor a típusértéket alkotó elemek számára egyszerre és egyben lefoglaljuk a szükséges memóriaterületet, amely aztán már nem változik, állandó marad. Ez a statikus reprezentáció. Mivel ilyenkor a memóriafoglalásra tömbszerűen, azaz közvetlenül egymás után (szekvenciálisan) kerül sor, ehhez tömböt vagy rekordot szoktunk használni. A lefoglalt területen belül egy-egy adatelem elérése annak pozíciója alapján történik, és ezt a pozíciót többnyire valamilyen aritmetikai képlet alapján közvetlenül ki lehet számolni. Emiatt szokták ezt a reprezentációs technikát szekvenciális vagy aritmetikai reprezentációnak is hívni. A másik lehetőség egy összetett érték tárolására az, hogy az egyes adatelemek memóriafoglalását egyenként, különböző helyen (gyakran különböző időpontokban) végezzük el, de minden helyfoglalásnál külön eltároljuk azt is, hogy mely címen találhatóak az éppen lefoglalt elemmel közvetlen kapcsolatban álló részek. Az így szétszórt memóriafoglalásokat tehát a címeik segítségével tudjuk összefogni, azaz az adatelemeket össze 541

542 tudjuk láncolni. Ezzel a technikával egészen bonyolult adatszerkezeteket lehet nagyon rugalmasan megvalósítani, hiszen így egy folyamatosan változó, dinamikus reprezentációhoz jutunk, amelyet sokszor szétszórt vagy láncolt reprezentációnak is neveznek. Némi zavart okozhat a fenti elnevezésekben az, hogy két különböző fogalmi szinten is használjuk a statikus illetve dinamikus jelzőket: külön a típus szerkezetére és külön annak megvalósítási módjára. A két szint között erős kapcsolat van, de ugyanakkor nem törvényszerű az, hogy egy dinamikus adatszerkezetű típus konkrét megvalósításához dinamikus reprezentációt, egy statikus szerkezethez pedig statikus reprezentációt lehetne csak használni. Tovább bonyolítja a terminológiát, hogy létezik egy harmadik fogalmi szint is, nevezetesen az, hogy a programozási nyelveknél a változók memóriafoglalási módjának meghatározására (és ebből következően a változó élettartamára) is ugyanezeket a jelzőket használjuk. Egy statikus memóriafoglalású változó rögtön a program elindulásakor helyet foglal a memóriában és végig ott marad, a dinamikus memóriafoglalásnál pedig speciális utasítások segítségével történik a foglalás és a felszabadítás. (Ezeken kívül az automatikus memóriafoglalást ismerjük még.) Fontos megemlíteni, hogy a dinamikus memóriafoglalás nyelvi elemeit nem csak a dinamikus reprezentáció megvalósításához lehet felhasználni. A három fogalmi szint dinamikus és statikus jelzőinek lehetséges társításai közül a legtöbb újdonságot kétség kívül a dinamikus szerkezetű típusok dinamikus reprezentációjának dinamikus nyelvi elemek felhasználásával történő megvalósítása ígéri. Ezért erről lesz ebben a fejezetben. Implementációs stratégia Fontos implementációs kérdés, hogy egy összetett adatszerkezetű típust statikusan vagy dinamikusan reprezentáljunk-e. Mivel egy típus viselkedése a műveleteinek hatásától függ, könnyen előfordul, hogy két azonos viselkedésű (azonos típusspecifikációjú) típus közül az egyik statikus, a másik dinamikus reprezentációjú, ezek kölcsönösen megvalósítják egymást, és nekünk ki kell 542

543 választani az egyiket. Hogy éppen melyiket, azt mindig a konkrét feladat megkötései határozzák meg. Például egy változó hosszúságú (tehát dinamikus szerkezetű) sorozat típusára adhatunk statikus, azaz szekvenciális reprezentációt úgy, hogy lefoglalunk egy kellő hosszúságú tömböt a sorozat elemeinek számára, és külön tároljuk, hogy a tömb első hány darab eleme reprezentálja az éppen aktuális sorozatot. Ennél azonban implementációs korlátot jelent az, hogy az ábrázolható sorozatok hosszai nem léphetik át a lefoglalt tömb méretét. Ha ilyen korlátozás bevezetését nem engedi meg a megoldandó feladat, akkor nem ezt, hanem egy dinamikus reprezentációt kell alkalmaznunk. Ez lehet például az előző tömbös megvalósításnak egy olyan változata, amelyben ha a tömb mérete kicsinek bizonyul, akkor lefoglalunk egy nagyobbat, ahová majd átmásoljuk az eddigi elemeket, de természetesen ez némi időveszteséggel jár. Megoldható azonban a sorozat dinamikus ábrázolása egy láncolt listával is, amely szétszórva egyenként tárolja a sorozat elemeit, és minden elem mellett tárolja a soron következő elem címét is. Ehhez a lánchoz bármikor lehet újabb elemet hozzáfűzni anélkül, hogy a meglévő összes elemet át kellene mozgatni, viszont sokkal tovább tart mondjuk a huszadik elem kiolvasása, mint az előző tömbös megoldásokban. Ez abban az esetben lesz különösen hátrányos, ha a konkrét feladatban sokszor kell a huszadik elemre hivatkozni, miközben a sorozat hossza csak ritkán nő meg. Egy statikus reprezentáció általában több implementációs korlátozást vezet be, mint a dinamikus, viszont az adatszerkezet egy elemének elérése többnyire sokkal gyorsabb. Tömbszerűen, közvetlenül egymás után elhelyezett elemek a tömbbeli pozíciójuk alapján ugyanis konstans futási idő alatt elérhetők. Ezzel szemben a dinamikus reprezentáció, különösen a láncolt reprezentáció egy sokkal rugalmasabb adatábrázolást tesz lehetővé, de az egyes elemek elérése általában tovább tart, mert csak az elemek közötti kapcsolatok mentén végig haladva lehet egy adott elemhez eljutni. Bárhogyan is valósítunk meg egy összetett szerkezetű adatot, az minden esetben gyűjteményként (tárolóként) viselkedik, ezért különösen érdekes az, hogyan lehet az elemi értékeit felsoroltatni, bejárni. Ez a bejárás a dinamikusan megvalósított összetett szerkezetű típusoknál nem egyszerű, de ha meg tudjuk valósítani a felsorolót, akkor az elemeik feldolgozására 543

544 könnyedén alkalmazhatjuk a felsorolókra általánosított programozási tételeket is. (lásd első kötet) Egyirányú lista fejelemmel fejelem h nil Egyirányú lista fejelem nélkül h nil ábra. Egyirányú láncolt listák Jellegzetes, a típusok számítógépes megvalósításánál használatos, dinamikus adatszerkezet az egyirányú láncolt lista, amely sorban egymás után elhelyezkedő változó számú adatelem ábrázolására szolgál. Az egyes adatelemeket szétszórva tároljuk a memóriában, de minden adatelem mellett eltároljuk a rákövetkezőjének a címét (legutolsó adatelemnél ez a sehová sem mutató nil értékű cím lesz). A láncolt lista tehát adatelem-cím párokat tartalmazó úgynevezett listaelemek sorozata, ahol a címek fűzik fel egy képzeletbeli láncba a listaelemeket. Megkülönböztetjük az úgynevezett fejelemes illetve fejelem nélküli változatát. A fejelemes változatban a láncolt lista legelső eleme egy speciális listaelem, az úgynevezett fejelem, amely mindig létezik, adatot azonban nem tartalmaz, egyetlen szerepe van, a lista első elemének címét (vagy üres lista esetén a nil-t) tartalmazza. Találkozhatunk kétirányú láncolt listákkal is, ahol egy listaelem nemcsak a rákövetkező elemnek, hanem a megelőző elemnek a címét is tartalmazzák, ciklikus láncolt listákkal, ahol a legutolsó elem rákövetkezője az első. A ciklikus kétirányú láncolt listában a legelső elem megelőzője az utolsó, az utolsó rákövetkezője pedig az első lesz. Találkozhatunk a láncolt listáknál még bonyolultabb láncolt szerkezetekkel is. A bináris fa láncolt ábrázolásánál a csúcsokat reprezentáló 544

545 listaelemeknek is legalább két címrésze van: az egyik a baloldali, a másik a jobboldali gyerekcsúcsot leíró listaelem címe. nil nil nil nil nil nil nil nil ábra. Jellegzetes láncolt reprezentációk: egy ciklikus kétirányú láncolt lista és egy bináris fa Nyelvi elemek A programozási nyelvek különféle mértékben támogatják, hogy a programozó egyénileg vezérelje egy adat számára történő memóriafoglalást illetve annak felszabadítását. A C++ nyelv a C-s hagyományoknak megfelelően lehetőséget biztosít a közvetlen memóriafoglalásra és felszabadításra. A lefoglalt memóriaterület címét egy pointerváltozóban lehet tárolni. Más nyelvek (Java, C#) megpróbálják elfedni a memóriakezelést, pointerváltozók helyett bevezetik a referencia-típusú változók fogalmát, a lefoglalt, de már nem használt memóriafoglalások megszüntetését pedig egy automatikus felszabadító mechanizmusra 545

546 (garbage collector) bízzák. A továbbiakban a C++ nyelvi lehetőségeit tárgyaljuk. A pointerváltozó egy közönséges változó, amely egy olyan memóriaterület címét veheti fel értékül, ahol a pointerváltozó típusának megfelelő értéket lehet tárolni. A pointerváltozóban tárolt memóriacím számára automatikusan jön létre helyfoglalás, de azt a területet, amelyre ez a cím mutat (amelynek címét majd a pointerváltozó őrzi) a programozónak külön utasítással (new) kell lefoglalnia a dinamikus memóriából (heap), és ha már nincs rá szükség, felszabadítania (delete). A pointerváltozó segítségével közvetett módon tudunk hivatkozni egy általunk lefoglalt memóriaterületen tárolt értékre, amelyet egy név nélküli változó értékének tekinthetünk. pointerváltozó név típus cím memória cím érték ábra. Pointerváltozó memóriafoglalása Az int *p deklaráció például egy olyan p pointerváltozót vezet be, amelyben tárolt címen egy egész típusú értéket helyezhetünk el, de csak az után, hogy lefoglaltuk számára a szükséges memóriaterületet. A p = new int értékadás végzi el a helyfoglalást, amely végrehajtása során kijelölődik a dinamikus memóriában egy egész értéket tárolni képes memóriaterület (több bájt), és ennek címe a p pointerváltozóba kerül. Ezután és csak ezután a *p kifejezéssel hivatkozhatunk a lefoglalt területen tárolt egész számra. A *p=12 vagy *p=*p+1 mind-mind értelmes értékadások erre a dinamikusan létrehozott, egész típusú, de név nélküli változóra. Ha már nincs szükség a dinamikus változóra, akkor a delete p utasítással megszüntethetjük a 546

547 helyfoglalását. Maga a p változó ettől még nem szűnik meg, de helyfoglalás hiányában a *p hivatkozás ezután már illegális, futás közben hibához vezet. Megtehetjük azt is, hogy egy int i változó címét (ennek jele: &i) egy int *p pointerváltozóban tároljuk el (p = &i), és ezt követően az i változó tartalmára *p alakban is tudunk majd hivatkozni. Egy int *p pointerváltozó arra is alkalmas, hogy egy konstans vagy futásközben automatikusan lefoglalt tömb (int t[n]) elemeire hivatkozni lehessen a segítségével. A p=t értékadás után a p pointer a tömb első (nulladik indexű) elemére mutat majd, azaz a *p és a t[0] ugyanazon memóriacímen található értéket jelenti. Ezekkel egyenértékű a p[0] és *t is. A p+2 kifejezés a tömb harmadik (2. indexű) elemére mutat (feltételezve, hogy van legalább három eleme a tömbnek), mert azt a memóriacímet adja meg, amelyik a p-ben tárolt címhez képest kétszer annyi byte-tal mutat hátrébb, ahány egy integer tárolásához kell. Így a *(p+2) ugyanarra a tömbelemre hivatkozik, mint a t[2] (vagy a *(t+2) vagy a p[2]). (Ha a tömbnek nem foglaltunk volna le legalább három elemet, a p+2 cím akkor is értelmes, megnézhetjük, mi található ott, és a *(p+2) visszaadja az ezen a címen kezdődő int-hez szükséges számú bájtból kiszámolható egész számot, sőt ez a szám meg is változtatható. Természetesen ez súlyos futási hibákat okozhat.) A fentiekből kikövetkeztethető az is, hogy egy tömbváltozó éppen a tömb első (nulladik indexű) elemének címét tartalmazza, azaz t == &t[0]. Egy automatikus helyfoglalású tömb a verem memóriában (stack) foglal helyet, ezért nem alkalmas arra, hogy egy alprogramban hozzuk létre és onnan adjuk vissza a hívás helyére, hiszen az alprogram befejeződésekor törlődik. Ha viszont a helyfoglalást a dinamikus memóriában végezzük, akkor az mindaddig ott marad, amíg a program be nem fejeződik, vagy fel nem szabadítjuk. Egy dinamikus helyfoglalású tömb készítésekor a programozónak először egy pointerváltozót kell definiálnia, majd külön utasítással (new) kell a dinamikus memóriában a tömb elemeinek helyet foglalni és e helyfoglalás kezdőcímét a pointerváltozónak értékül adni. Egy dinamikus helyfoglalású tömbváltozó tehát egy pointer, amely a lefoglalt tömbterületre, pontosabban annak legelső elemére mutat (legelső bájtjának címét tartalmazza). Ezt a pointerváltozót (ahogy minden 547

548 pointerváltozót) tömbként használhatjuk, azaz utána írva az indexelő operátort hivatkozhatunk a pointerváltozóban megadott cím után elhelyezkedő valahányadik elemre. int n; cin >> n; int* v = new int[n]; verem memória (STACK) dinamikus memória (HEAP) v int* cím cím ábra. Dinamikus helyfoglalású tömb a memóriában A lefoglalt terület felszabadítása sem automatikus, azt a felhasználónak kell egy külön utasítással (delete[] v) kezdeményezni. Hangsúlyozzuk, hogy a dinamikus helyfoglalású tömbök esetében, akárcsak az automatikus helyfoglalásúaknál, a helyfoglalás után a tömb méretén már nem lehet változtatni. Természetesen nincs akadálya annak, hogy egy futás közben dinamikusan változó méretű tömböt készítsünk (bár elméletben ezt már nem nevezhetjük tömbnek, hiszen annak egyik alaptulajdonsága, hogy a mérete állandó), ha méretváltozás esetén új memóriaterületet foglalunk le, ahová átmásoljuk a tömb eddig tárolt elemeit, majd a régi területet felszabadítjuk. (Tulajdonképpen a vector<> egy ilyen jellegű tömb, csak elrejti előlünk a dinamikus memóriakezeléssel járó nehézségeket.) 548

549 Többdimenziós tömbök, például egy mátrix esetén az automatikus helyfoglalással az elemek sorfolytonosan egymás után kerülnek elhelyezésre a memóriában. A dinamikus helyfoglalás esetén ez nem egészen van így. A folyamat egyrészt két lépcsőben történik, másrészt a mátrix egyes sorai külön-külön kerülnek elhelyezésre a dinamikus memóriában. Ezek egyben tartásához először le kell foglalni külön egy egydimenziós tömböt a sorok memóriacímeinek tárolására ez tehát egy pointertömb lesz, majd különkülön foglaljuk le a mátrix sorait, mint egydimenziós tömböket, amelyeknek kezdőcímét, a pointertömb megfelelő elemének adjuk értékül. Maga a mátrix egy pointerváltozó, amely a pointertömb elejére mutat. verem memória (STACK) dinamikus memória (HEAP) v int** cím cím cím cím cím ábra. Dinamikus helyfoglalású mátrix a memóriában Fenti konstrukció lehetőség ad eltérő elemszámú sorok létrehozására is. Az ilyen kétdimenziós tömböt szokták kesztyű mátrixnak nevezni. int** w; w= new int*[3]; for(int i=0; i<3; ++i) w[i]= new int[4]; A dinamikus helyfoglalású tömbök műveletei megegyeznek az automatikus helyfoglalású tömbökével. Érdekesség, hogy egy mátrixelemre pointeraritmetikai kifejezéssel is hivatkozhatnánk. Például a w[i][j] helyett használhatjuk a *(*(w+i)+j) kifejezést. Ennél hasznosabb lehetőség, hogy 549

550 a w[i] a mátrix i-edik sorát, mint egydimenziós dinamikusan lefoglalt tömböt azonosítja. Súlyos hiba, és még futási időben is rejtve maradhat egy ideig, ha egy dinamikus helyfoglalású tömbnek olyankor hivatkozunk az elemeire, amikor azok még nincsenek lefoglalva vagy már fel lettek szabadítva. A dinamikus helyfoglalású tömb felszabadításáról a programozónak kell gondoskodni. Kétdimenziós tömbök esetén az elemek felszabadítása is két lépcsőben történik, csak a lefoglalással ellentétes sorrendben. for(int i=0; i<3; ++i) delete[] w[i]; delete[] w; A fentiek mintájára kettőnél több dimenziós dinamikus helyfoglalású tömbök is készíthetők. Egy egyirányú láncolt lista listaelemeinek típusát (ami egy rekord) C++ nyelven a struct segítségével írhatjuk le. Az alábbi példában a listaelem tartalma egész szám lesz, mutató része pedig egy listaelemre mutató pointer. A struct az osztályokhoz hasonlóan konstruktorral is rendelkezhet. (A struct tagjai szemben az osztályokkal hivatalból publikusak.) A NULL a sehová sem mutató nil pointerértéket reprezentálja. struct Node { int value; Node *next; Node(int i=0, Node *q=null): value(i), next(q){ ; Az alábbi három utasítás a konstruktor paraméterváltozóinak alapértelmezése miatt egyenértékű: olyan listaelemet hoznak létre, amely a 0 egész számot és a nil címet tartalmazza, magának a listaelemnek címe pedig a p pointerváltozóba kerül. 550

551 Node *p = new Node(); Node *p = new Node(0); Node *p = new Node(0, NULL); A láncolt listákon végezhető egyik alapművelet egy új listaelem beszúrása illetve. A beszúrás az új elem létrehozásából, értékének kitöltéséből és a láncba történő befűzéséből áll. Egy fejelemes lista esetén mindig egy már létező listaelem mögé tudjuk beszúrni az listaelemet, ezért ha mondjuk egy u pointerváltozó erre a már létező listaelemre mutat (annak címét tartalmazza), akkor az alábbi kód végzi a 23 értéket tartalmazó listaelem beszúrását: Node *p = new Node(23,u->next); u->next = p; Fejelem nélküli lista esetében előfordulhat, hogy a legelső listaelem elé kell egy új listaelemet beszúrni. Ezt az előzőtől némileg eltérő kód végzi el, ahol feltesszük, hogy a legelső listaelem címét a h pointer őrzi: h = new Node(23,h->next); A másik alapművelet egy listaelemnek a törlése egy láncolt listából. Ehhez először ki kell fűzni ezt az elemet, majd törölhetjük a dinamikus memóriából. Egy listaelemet a delete p utasítás törli, ha a p pointer a listaelemre mutat. Tegyük fel, hogy az u pointerváltozó mutatja azt a listaelemet, amely utáni listaelemet ha van olyan egyáltalán kell kitörölnünk: Node *p = u->next; if(p!= NULL){ u->next = p->next; delete p; 551

552 Fejelem nélküli láncolt lista esetében szóba jöhet a legelső listaelem törlése is, amelyre a h pointerváltozó mutat: if(h!= NULL){ Node *p = h; h = h->next; delete p; A fentiek alapján már könnyen felépíthető illetve lebontható egy teljes láncolt lista. Az alábbi kód egy n elemű láncolt lista felépítését mutatja, ahol az elemek rendre az 1-től n-ig terjedő egész számokat kapják meg értékül. A lista elejére a h pointer mutat majd, az u és a p segédpointerek. Fejelemmel Node *h = new Node(); Fejelem nélkül Node *h = new Node(1); Node *u = h; Node *u = h; for(int i=1;i<n;i++){ Node *p = new Node(i); u->next=p; for(int i=2;i<n;i++){ Node *p = new Node(i); u->next=p; u = p; u = p; 552

553 A láncolt lista lebontása egyformán történik a fejelemes és fejelem nélküli változatokra. Ha a h pointer a lista legelső elemére mutat, azaz annak címét tartalmazza, akkor while(h!= NULL){ Node *p = h; h = p->next; delete p; Fontos tevékenység egy láncolt lista bejárása. Ilyenkor a felsoroló objektum egy listaelem címét tartalmazó pointerváltozó, amely a bejárás során mindig az aktuális listaelemre mutat, így annak értéke kiolvasható (Current()). Kezdetben a lista első elemére állítjuk a felsorolót (First()), a rákövetkező listaelem címét az aktuális listaelemből olvashatjuk ki (Next()), a felsorolás végét pedig az jelzi, ha a felsoroló a nil értéket veszi fel (End()), amelyet lista legutolsó listaeleme tárol következő címként. Az így megvalósított bejárás segítségével a tanult programozási tételek könnyen alkalmazhatóak a láncolt listában tárolt értékek feldolgozására. A fejelemes illetve fejelem nélküli esetek csak a First() művelet megvalósításában térnek el. Fejelemmel First() ~ p = h->next End() ~ NULL == p Current() ~ p->value Next() ~ p = p->next Fejelem nélkül ~ p = h ~ NULL == p ~ p->value ~ p = p->next ábra. Egyirányú lista felsoroló műveletei 553

554 A 11. fejezetben bevezettük az egyszerű osztály fogalmát, amelyeket különösebb óvintézkedések nélkül lehetett implementálni. Azok az osztályok azonban, amelyekben közvetlen dinamikus memóriakezelést végezünk, azaz amelyeknek pointer adattagjai is vannak (pointer adattagoknak közvetlen memóriafoglalással adunk értéket, hogy aztán az így lefoglalt memóriaterületen tárolt értékre hivatkozhassunk) csak bizonyos szabályok betartása mellett használhatók biztonságosan. Az egyik szabály az osztály destruktorára vonatkozik. Egy objektum alapértelmezett megszüntetésekor egy pointer adattag, amelyik a dinamikus memóriából általunk lefoglalt területnek címét tartalmazza, automatikusan megszűnik ugyan, de az általa mutatott lefoglalt terület továbbra is foglalt marad, bár már nem lesz használatban. A memóriaterületnek ez része más célra sem használható, lényegében elveszik (ez a memória-szivárgás jelensége). Mivel C++ nyelvben nincs automatikus felszabadító mechanizmus, a destruktorban amely egy objektum megszűnésekor automatikusan meghívódik nekünk kell gondoskodnunk az ilyen dinamikus helyfoglalások felszabadításáról. Korábban rámutattunk a konstruktor és destruktor között fennálló egyensúlyra (amit a konstruktor létrehoz, felépít, megnyit, azt a destruktor lezár, lebont, megszűntet). Ez most annyiban módosul, hogy a destruktornak nemcsak a konstruktorban végzett memóriafoglalásokat, hanem az összes metódusban végzett memóriafoglalást fel kell szabadítania. Minden osztály hivatalból rendelkezik egy másoló konstruktorral és egy értékadás operátorral. Az egyik meglevő objektum másolataként létrehoz egy új objektumot, illetve egy létező objektumot egy másik objektummal tesz egyenlővé. Ezek e tevékenységek az adattagok szintjén hajtódnak végre, azaz a megfelelő adattagok között kerül sor értékadásra. Ha az adattag egy pointer, akkor az eredeti objektum ezen tagjában tárolt memóriacím is lemásolódik és az új (értékadás esetén a másik) objektum azonos nevű pointere ezt a címet veszi fel értékül. Ennél fogva mindkettő objektum azonos nevű pointer tagja ugyanarra a memóriaterületre fog mutatni, azaz két látszólag független objektum közös memóriaterületen osztozik. Sőt értékadás esetén az értékadás baloldali objektumának a megfelelő pointer-adattagjában tárolt korábbi cím is elvész, az azon található adatok elérhetetlenek lesznek (adatvesztés), de foglalják a dinamikus memóriát (memória-szivárgás). 554

555 A problémára két megoldás van. Vagy megtiltjuk a másoló konstruktor és az értékadás operátor használatát, vagy elkészítjük azok helyes változatait. A tiltás betartását úgy szavatolhatjuk, hogy a másoló konstruktor és az értékadás operátor deklarációját privát tagként vesszük fel az osztályba, így ha véletlenül mégis sor kerülne valamelyik hívására, azt a fordítóprogram felismeri és fordítási hibát fog jelezni. A másik esetben publikusnak deklaráljuk ezeket a metódusokat, és újra kell definiálni azokat. A másoló konstruktornak mély másolást kell végeznie, azaz egy pointer-adattag által mutatott memória területen tárolt adatoknak új helyet kell foglalni, oda az adatokat át kell másolni, és ennek az új területnek a címét kell eltárolni a lemásolt objektum megfelelő pointer-adattagjában. Sőt, ha a lefoglalt terület egy eleme maga is egy pointer, akkor az ő általa mutatott területet is le kell a fenti módon másolni. A helyes értékadás operátor definíciójához egy sokszor alkalmazható recept az, ha azt az alábbi négy lépésből álló tevékenységként adjuk meg. Először megvizsgáljuk, hogy az értékül adott objektum nem egyezik-e meg az értékadás baloldalán álló objektumával, röviden az értékadás objektumával, ilyenkor ugyanis nem kell tenni semmit, önmagának értékül adni egy objektumot nem kíván semmi tennivalót. Ha különböznek, akkor a destruktor mintájára megszüntetjük az értékadás objektumát, majd a másoló konstruktor mintájára újra létrehozzuk azt az értékül adandó objektum alapján. Végül gondoskodunk arról, hogy az értékadás operátor visszatérési értékként visszaadja az értékül adott objektum egy hivatkozását. 555

556 33. Feladat: Verem Olvassunk be a szabványos bementről egész számokat, és írjuk ki őket fordított sorrendben a szabványos kimenetre! A megoldáshoz használjunk vermet! Specifikáció A feladat lényegében egy sorozat megfordítása. A = ( cin : Z*, cout : Z* ) Ef = ( cin =cin ) cin' Uf = ( cout i 1 cin' cin' i 1 ) Mivel mind a bemeneti, mind a kimeneti sorozat speciális, az egyik a szabványos be-, a másik a kimenet, ezért az utófeltétel által sugallt megoldással szemben egy vermet fogunk segédadatként használni. Első menetben bepakoljuk a verembe a szabványos bemenetről érkező számokat, majd egy második menetben kiürítjük a szabványos kimenetre a verem tartalmát. Figyelembe véve a verem alaptulajdonságát, mely szerint a legutoljára betett elemet adja vissza legelőször (LIFO last in first out) éppen a kívánt feladatot oldjuk meg. A vermet nem kell bemutatni az Olvasónak. Ez az a nevezetes adatszerkezet, amelyik típusára úgy gondolhatunk, amelynek típusértékei olyan sorozatok, amelyeknek az elejéhez (tetejéhez) lehet hozzáfűzni újabb elemet (push), az elejéről lehet egy elemet kivenni (pop) illetve kiolvasni (top) és meg lehet nézni, hogy a sorozat üres-e (empty). A mi esetünkben egész számok tárolására alkalmas veremre lesz szükségünk. s:stack(z) s.push(e) s.pop() e:=s.top() l:=s.empty() v:z* v := <e, v> v := <v 2,,v v > e:=v 1 l:= v 556

557 Absztrakt program A megoldó program két szintre tagolódik. A felső szinten létrehozunk egy Stack típusú objektumot, amelyre meghívhatjuk a Push(), Pop(), Top() és Empty() műveleteket, de nem törődünk azzal, hogyan kell a vermet megvalósítani. A verem műveletekkel, valamint a beolvasás és kiírás műveleteivel könnyen elkészíthető a feladatot megoldó két ciklus szekvenciája: az első feltölti a vermet, a második kiüríti. Az alábbi struktogrammban kivételesen a C++ nyelvben megszokott jelölésekkel hivatkozunk a beolvasás és kiírás műveleteire. cin >> e cin.fail() s.push (e) cin >> e s.empty() cout << s.top () s.pop() A megoldás alsó szintje a verem típusának megvalósítását tartalmazza. A verem fizikai megvalósításakor a vermet reprezentáló sorozatot kell megfelelő módon ábrázolni a memóriában. Két klasszikus megoldást szoktak erre alkalmazni: az egyik egy egybefüggő tömb segítségével ábrázolja a sorozat elemeit, a másik egy fejelem nélküli láncolt listában. Az előbbi előnye a kompakt tárolás, hátránya, hogy a tömb létrehozásakor rögzíteni kell egy korlátot a verembe helyezendő elemek maximális számára nézve. Az utóbbi előnye, hogy a verem méretét nem kell előre rögzíteni, hátránya viszont, hogy a listaelemek a tárolt elem mellett egy memória címet is tartalmaznak, így a memória igénye ennek a megoldásnak nagyobb. A veremműveletek futási ideje azonban mindkét esetben konstans. 557

558 Amikor tömbben ábrázoljuk a verem elemeit, akkor külön nyilván kell tartanunk a legutoljára betett elem tömbindexét. Ez mutatja a verem tetején levő elem tömbbeli helyét. Ha csak a verem tetején található értékre van szükségünk (Top()), akkor a tömb ennyiedik elemét kell kiolvasni. Újabb érték verembe helyezésekor (Push()) eggyel növeljük ennek a tömbindexnek az értékét, és az így kapott helyre tesszük be az értéket. Ha az index a tömb legutolsó elemére mutat, akkor a verem megtelt, újabb elemet nem helyezhetünk el benne, hacsak nem másoljuk át az egészet egy nagyobb tömbbe. Érték kivételekor (Pop()) az index értékét eggyel csökkentjük. Ha az index a tömb előtti pozícióra mutat, akkor a verem üres (Empty()), nem lehet belőle értéket elhagyni. Két konstruktort fogunk bevezetni. Az egyik egy 10 értéket befogadni képes tömböt fog létrehozni a verem számára, a másiknak paraméterként lehet majd megadni a tömb méretét. Amikor láncolt listával valósítjuk meg a vermet, akkor a verem tetején levő értéket az első listaelem tartalmazza. Magára a listára a lista első elemének címével hivatkozunk, amelyet egy pointer változóban tárolunk. Ha ennek értéke nil, akkor a lista (és így a verem is) üres (Empty()), egyébként a verem tetején levő értéket tartalmazó listaelemre mutat (Top()). Egy új érték verembe helyezésekor (Push()) egy új listaelemet fűzünk a láncolt lista elé, veremből való kivételkor (Pop()) feltéve, hogy a láncolt lista nem üres kifűzzük a lista első elemét. A verem típusát egy osztály segítségével definiáljuk. Ennek az osztálynak a publikus része mindkét megvalósításnál ugyanaz kell legyen legfeljebb csak a konstruktorok lehetnek eltérőek. A privát rész a reprezentációt tükrözi, ez tehát különbözik a két megvalósításnál, és természetesen ettől függ a műveletek implementációja is. Tekintettel a feladat egyszerű voltára, ezeket közvetlenül az C++ kód segítségével mutatjuk be. A megvalósításban megengedjük, hogy a Pop() művelet ne csak kivegye, hanem vissza is adja a verem tetején levő értéket, ennél fogva a megoldó programban nem kell majd a Top() műveletet használni. 558

559 Implementálás A program komponens szerkezete main.cpp stack.h-stack.cpp main() class Stack Stack() ~Stack() Push() Pop() Top() Empty() ábra. Komponens szerkezet A main.cpp main függvénye tartalmazza az absztrakt megoldás felső szintjét. Ez a program közvetlenül hivatkozik a Stack osztály metódusaira, ezért a main.cpp állományba be kell inklúdolni a stack.h állományt. Főprogram kódolása Nem igényel különösebb magyarázatot a feladat megoldását végző két ciklus szekvenciáját tartalmazó main függvény. Ez a tervezésnél megadott struktogramm C++ nyelvű kódját tartalmazza. Az absztrakt program első ciklusának kódolásakor kihasználjuk azt, hogy a cin >> i hibás működése nemcsak a cin.fail() segítségével kérdezhető le, hanem maga a cin >> i utasítás ad vissza ilyenkor hamis értéket. Ezért a while(cin >> i){

560 kódot is alkalmazhatjuk a cin >> i; while(!cin.fail()){... cin >> i; kód helyett. A függvény első sora tömbös megvalósítású verem esetén lehetne a kommentként megadott utasítás is. Első esetben egy előre rögzített (most 10) maximális elemszámú verem jön létre, a második kommentként jelzett esetben paraméterként adható meg a verem elemszámára adott felső korlát. A láncolt listás megvalósítás esetén a kommentben feltüntetett utasítás nem alkalmazható. Stack s; // Stack s(100); int i; while(cin >> i){ try{ s.push(i); catch (Stack::Exceptions e){ if (Stack::FULLSTACK == e) cout << "A verem megtelt\n"; 560

561 while(!s.empty()){ cout << s.pop() << endl; Az első ciklusban (a verem feltöltésénél) kivételkezelést találunk. Tömbös megvalósítás esetén a FULLSTACK kivétel akkor keletkezik, amikor a tömb már tele van a verembe rakott elemekkel, és egy újabb elemet akarunk betenni. (Ugyanezt a kivételt kellene lekezelni akkor is, ha a paraméteres konstruktorral hozunk létre vermet, de a paraméterként megadott méret olyan nagy, hogy ahhoz már nincs elég memória.) Láncolt listás megvalósítás esetén is van létjogosultsága ennek a kivételnek, hiszen egy újabb listaelem létrehozásánál már nem biztos, hogy van elég szabad hely az alkalmazás számára biztosított dinamikus memóriaterületen. Verem típus osztálya A verem-típus megvalósítására tehát kétféle osztályt mutatunk: az egyikben egy tömb segítségével ábrázoljuk a vermet, a másiknál ehhez egy láncolt listát fogunk használni. Az osztályok publikus interfésze mindkét esetben ugyanaz: a konstruktor és destruktor mellett a szokásos verem-műveleteket kínálják fel. Még a verem-osztály hibás felhasználás esetén dobott kivételei is azonosak a két verzióban. Ezeket a publikus Exceptions felsorolt típus tartalmazza. A verem akkor dob EMPTYSTACK kivételt, ha egy üres veremből ki akarunk venni egy értéket, vagy egy üres verem tetején levő értéket akarunk használni (Pop()és Top()műveletek). A FULLSTACK kivételnél vagy egy tömbös reprezentációjú teli verembe akarunk újabb értéket betenni (Push() művelet) vagy a dinamikus memóriában végrehajtott memóriafoglalás nem sikerül (tömbös reprezentáció esetén a konstruktorban, láncolt listás esetben a Push() műveletben). Egyetlen apró különbséget mutat a kétféle megvalósítás publikus része: a tömbös megvalósításhoz két konstruktort is biztosítunk. 561

562 Verem típus tömbös megvalósítása Az osztály publikus része a kivételeket, két konstruktort, a destruktort és a verem műveletek metódusait deklarálja. class Stack{ public: enum Exceptions{EMPTYSTACK, FULLSTACK; Stack(); Stack(int s); ~Stack(); void Push(int e); int Pop(); int Top() const; bool Empty()const; Az osztály rejtett részében adjuk meg a verem maximális méretét tartalmazó adattagot (size), a verem értékeit tartalmazó dinamikus helyfoglalású tömböt (vect) és a verem tetejét jelző indexet (top). private: Stack(const Stack&); Stack& operator=(const Stack&); 562

563 void Allocate(int n); int size; int* vect; int top; ; A verem reprezentációja tehát már itt is dinamikus (habár az eredendően dinamikus adatszerkezetű verem reprezentációja most statikus), ezért az alapértelmezett másoló konstruktor és az értékadás operátor nem működik megfelelően. (Szerencsére a főprogram nem is használja ezeket a metódusokat.) Ha azonban egy vermet egy létező verem másolataként hoznánk létre, akkor a másoló konstruktor nem másolná le a vect által kijelölt dinamikusan lefoglalt tömböt, hanem ugyan azt a tömböt használná az új verem is. Ebben a változatban nem akarunk a helyes másoló konstruktor és értékadás operátor elkészítésével bajlódni (legyen ez az Olvasó feladata), de hogy ne érjen bennünket meglepetés, mindkettőt privát metódusként deklaráljuk újra. Ennél fogva, ha mégis másolni akarnánk egy vermet, vagy egy függvénynek paraméterként (érték szerint) átadni, esetleg értékül adni, akkor fordítási hibát kapnánk. Az osztály két konstruktort tartalmaz. A paraméter nélküli konstruktor beépített módon egy legfeljebb 10 elemű tömböt (azaz vermet) hoz létre, a másiknak meg kell adni a tömb méretét. Mindkét konstruktor a privát Allocate()függvényt hívja meg, egyik a 10-et, másik a bemenő paraméterét adja át ennek, hogy lefoglalja a dinamikus memóriából a megadott méretű tömböt (dinamikus helyfoglalás). Ezt követően eltárolja a méretet a size adattagban és az üres veremre jellemző -1 kezdő értéket a top adattagban. A destruktor feladata felszabadítani a tömböt. Stack::Stack() { Allocate(10); Stack::Stack(int n) { Allocate(n); 563

564 void Stack::Allocate(int n) { try{ size = n; vect = new int[n]; top = -1; catch(std::bad_alloc o){throw FULLSTACK; Stack::~Stack(){ delete[] vect; A Push() művelet megnöveli a top értékét feltéve, hogy így nem lépi át a tömb indextartományának felső határát, mert különben FULLSTACK kivételt dob és a vect tömb top-adik pozíciójára beírja az új elemet. A Pop()művelet kiolvassa a vect tömb top-adik pozícióján található elemet, majd csökkenti a top értékét eggyel feltéve, hogy a top nem -1, mert ekkor EMPTYSTACK kivételt dob a metódus. A Top()művelet üres lista esetén EMPTYSTACK kivételt dob, egyébként a vect tömb top-adik pozícióján található elemet. Az Empty()művelet akkor ad igaz értéket, ha top értéke -1, egyébként hamis értékkel tér vissza. A Top() és az Empty()műveletek konstans metódusok, hiszen működésük során nem módosul a verem objektum. void Stack::Push(int e) { if( top+1 == size ) throw FULLSTACK; vect[++top]=e; 564

565 int Stack::Pop() { if( -1 == top ) throw EMPTYSTACK; return vect[top--]; int Stack::Top() const { if( -1 == top ) throw EMPTYSTACK; return vect[top]; bool Stack::Empty()const { return -1 == top; Verem típus egyirányú fejelem nélküli láncolt listás megvalósítása Az osztály publikus része egyetlen részletben különbözik az előző megoldástól: egyetlen paraméter nélküli konstruktora van. Most sort kerítünk a másoló konstruktor és az értékadás operátor implementációjára annak ellenére, hogy a főprogram nem használja ezeket. Ezek alapértelmezett változatai ugyanis rosszul működnének, ha mégis szükség lenne rájuk. class Stack{ public: enum Exceptions{EMPTYSTACK, FULLSTACK; 565

566 Stack(); ~Stack(); Stack(const Stack&); Stack& operator=(const Stack&); void Push(int e); int Pop(); int Top() const; bool Empty()const; Az osztály rejtett részében definiáljuk a listaelem típusát. Ezt követi a verem reprezentációja, ami nem más, mint a legelső listaelemre mutató head pointer. private: struct Node{ int val; Node *next; Node(int e, Node *n) : val(e), next(n){ ; ; Node *head; 566

567 A konstruktor egy üres vermet, tehát egy üres láncolt listát inicializál, ehhez beállítja a head értékét nil-re. A destruktor felszabadítja a vermet megvalósító lista összes listaelemét: Stack::Stack(): head(null){ Stack::~Stack() { Node *p; while(head!= NULL){ p = head; head = head->next; delete p; A Push()művelet létrehoz egy új listaelemet, és befűzi azt a lista elejére. Ha nem lehet újabb listaelemet lefoglalni, akkor egy memóriafoglalási kivétel (bad_alloc) dobódik, amit elkapunk, és FULLSTACK üzenetként dobunk tovább. Ez jelzi, hogy memória korlát miatt betelt a verem. void Stack::Push(int e) { try{ head = new Node(e,head); catch(std::bad_alloc o){ 567

568 throw FULLSTACK; A Pop() művelet üres lista esetén EMPTYSTACK kivételt dob, egyébként kifűzi a lista legelső elemét, majd felszabadítja azt, de visszaadja a kifűzött listaelem értékét a hívása helyére. int Stack::Pop() { if(null == head) throw EMPTYSTACK; int e = head->val; Node *p = head; head = head->next; delete p; return e; A Top()művelet üres lista esetén EMPTYSTACK kivételt dob, egyébként visszaadja a lista legelső elemében tárolt értéket. Az Empty() művelet üres lista esetén ad igaz értéket, egyébként hamis értékkel tér vissza. int Stack::Top()const { if(null == head) throw EMPTYSTACK; 568

569 return head->val; bool Stack::Empty()const { return NULL == head; A másoló konstruktor üres inicializáló verem-objektum esetén egy üres láncolt listát, egyébként egy új láncolt listát épít fel. Ez utóbbiban ugyanazokat az értékeket, ugyanolyan sorrendben helyezi el, mint amelyek lemásolandó verem láncolt listájában vannak, tehát egy lista-másolást végez. Stack::Stack(const Stack& s) { if(null == s.head) head = NULL; else { try{ head = new Node(s.head->val,NULL); catch(std::bad_alloc o){ throw FULLSTACK; Node *q = head; Node *p = s.head->next; while(p!= NULL){ try{ q->next = new Node(p->val,NULL); 569

570 catch(std::bad_alloc o){throw FULLSTACK; q = q->next; p = p->next; Az értékadás operátor feltéve, hogy az értékadás két oldalán található verem-objektumok nem azonosak az alapértelmezett verem objektum (ez az értékadás baloldali objektuma) által foglalt láncolt lista törléséből, majd az értékül adandó verem objektum láncolt listájának lemásolásából áll. Mindez kiegészül a megfelelő visszatérési érték megadásával. Stack& Stack::operator=(const Stack& s) { if(&s == this) return *this; Node *p; while(head!= NULL){ p = head; head = head->next; delete p; if(null == s.head) head = NULL; 570

571 else { try{ head = new Node(s.head->val,NULL); catch(std::bad_alloc o){throw FULLSTACK; Node *q = head; Node *p = s.head->next; while(p!= NULL){ try{ q->next = new Node(p->val,NULL); catch(std::bad_alloc o){throw FULLSTACK; q = q->next; p = p->next; return *this; Megfigyelhető, hogy az értékadás operátor lényegében a destruktorban és a másoló konstruktorban leírt kód szekvenciája, amely kiegészül egy kezdeti vizsgálattal (&s==this) és a végén egy speciális return *this utasítással. 571

572 Tesztelés Az alapfeladat érvényes fekete doboz tesztelése nem igényel túl sok tesztesetet. Egy általános bemenettel ellenőrizhetjük, hogy a kimenten fordított sorrendben jelennek-e meg az értékek. Természetesen kipróbáljuk nulla darab és egyetlen értékből álló bemeneteket is. Az érvénytelen tesztesetek kivételt eredményeznek. A főprogram nem ad lehetőséget az EMPTYSTACK kivétel keletkezésére, ezért érdemes erre egy külön tesztprogramot készíteni. Stack s; try{ s.top(); catch (Stack::Exceptions e){ if (Stack::EMPTYSTACK == e) cout << "A verem ures!\n"; A FULLSTACK kivétel könnyen kiváltható, ha a tömbös megvalósítású verem esetén tíznél több elemet akarunk elhelyezni a veremben. Azt is illik azonban kimérni, hogy legfeljebb mekkora tömböt lehet létrehozni. try{ int n; while(true){ cout << "A verem mérete: "; cin >> n; Stack s(n); catch (Stack::Exceptions e){ 572

573 if (Stack::FULLSTACK == e) cout << "Nincs elég memória!\n"; Láncolt listás megvalósítású verem esetben is egy speciális (nem interaktív) főprogramot készítünk ahhoz, hogy a verem betelt hibaüzenetet megkapjuk. 573

574 Stack s; int i; try{ while(true){ s.push(i++); catch (Stack::Exceptions e){ if (Stack::FULLSTACK == e) cout << "Nincs elég memória!" << "Listaelemek szama:" << i << endl; Az eredeti főprogram viszont nem alkalmas arra, hogy a Stack osztály komponens tesztjét elvégezzük. Ehhez célszerű egy menüválasztós tesztprogramot készíteni, amely lehetővé teszi ennek a komponensnek a fekete és fehér doboz tesztelését. Célszerű egy olyan metódussal is kiegészíteni az osztályt, amelyik meg tudja jeleníteni a verembe levő értékek sorozatát. Ezt végrehajtva minden művelet előtt és után pontos képet nyerhetünk a művelet működéséről. A fehér doboz teszthez hasznos lehet, ha ez a kiírás nemcsak a verembe levő elemek sorozatát adja meg, hanem annak ábrázolásával kapcsolatos egyéb adatokat is (tömbös megvalósításnál a top adattag értékét, láncolt listás esetben a pointer értékeket).a műveletek önmagukban véve igen egyszerűek, az Olvasó könnyen kitalálhat rájuk tesztadatokat. Ezen kívül a komponens teszt a műveletek variációs tesztjét is tartalmazza. Ilyen variációk lehetnek az alábbiak: 1. Ha üres, vagy bármilyen más veremre többször alkalmazzuk a Push() műveletet, majd ugyanannyiszor a Pop() műveletet, akkor vissza kell kapnunk a kiindulási vermet. 2. A Push() vagy a Pop() művelet egymás után nem hajtható végre akárhányszor ugyanazzal az eredménnyel. 574

575 3. A Top() vagy az Empty() művelet egymás után akárhányszor végrehajtható, eredménye nem változik. 575

576 Teljes program main.cpp: #include <iostream> #include "stack.h" using namespace std; int main() { Stack s; int i; while(cin >> i){ try{ s.push(i); catch (Stack::Exceptions e){ if (Stack::FULLSTACK == e) cout << "A verem megtelt\n"; while(!s.empty()){ cout << s.pop() << endl; return 0; stack.h: (tömbös változat) 576

577 #ifndef STACK_H #define STACK_H class Stack{ public: enum Exceptions{EMPTYSTACK, FULLSTACK; Stack(); Stack(int s); ~Stack(); void Push(int e); int Pop(); int Top() const; bool Empty()const; private: Stack(const Stack&); Stack& operator=(const Stack&); void Allocate(int n); int size; int* vect; 577

578 int top; ; #endif stack.cpp: (tömbös változat) #include "stack.h" #include <memory> Stack::Stack() { Allocate(10); Stack::Stack(int n) { Allocate(n); void Stack::Allocate(int n) { try{ size = n; vect = new int[n]; top = -1; catch(std::bad_alloc o){throw FULLSTACK; Stack::~Stack() { delete[] vect; void Stack::Push(int e) 578

579 { if( top+1 == size ) throw FULLSTACK; vect[++top]=e; int Stack::Pop() { if( -1 == top ) throw EMPTYSTACK; return vect[top--]; int Stack::Top() const { if( -1 == top ) throw EMPTYSTACK; return vect[top]; bool Stack::Empty()const { return -1 == top; stack.h: (láncolt listás változat) #ifndef STACK_H #define STACK_H class Stack{ public: 579

580 enum Exceptions{EMPTYSTACK, FULLSTACK; Stack(); ~Stack(); Stack(const Stack&); Stack& operator=(const Stack&); void Push(int e); int Pop(); int Top() const; bool Empty()const; private: struct Node{ int val; Node *next; Node(int e, Node *n) : val(e), next(n){ ; ; Node *head; #endif 580

581 581

582 stack.cpp: (láncolt listás változat) #include "stack.h" #include <memory> using namespace std; Stack::Stack(): head(null){ Stack::~Stack() { Node *p; while(head!= NULL){ p = head; head = head->next; delete p; void Stack::Push(int e) { try{ head = new Node(e,head); catch(std::bad_alloc o){ throw FULLSTACK; int Stack::Pop() { if(null == head) throw EMPTYSTACK; int e = head->val; 582

583 Node *p = head; head = head->next; delete p; return e; int Stack::Top()const { if(null == head) throw EMPTYSTACK; return head->val; bool Stack::Empty()const { return NULL == head; 583

584 Stack::Stack(const Stack& s) { if(null == s.head) head = NULL; else { try{ head = new Node(s.head->val,NULL); catch(std::bad_alloc o){ throw FULLSTACK; Node *q = head; Node *p = s.head->next; while(p!= NULL){ try{ q->next = new Node(p->val,NULL); catch(std::bad_alloc o){throw FULLSTACK; q = q->next; p = p->next; Stack& Stack::operator=(const Stack& s) { if(&s == this) return *this; 584

585 Node *p; while(head!= NULL){ p = head; head = head->next; delete p; if(null == s.head) head = NULL; else { try{ head = new Node(s.head->val,NULL); catch(std::bad_alloc o){throw FULLSTACK; Node *q = head; Node *p = s.head->next; while(p!= NULL){ try{ q->next = new Node(p->val,NULL); catch(std::bad_alloc o){throw FULLSTACK; q = q->next; p = p->next; return *this; 585

586 34. Feladat: Kettős sor A szabványos bemenetről érkező egész számokat szortírozzuk és írjuk ki a szabványos kimenetre úgy, hogy először a negatívokat, majd azt követően a többit jelenítjük meg! Ezen kívül minden kiírt szám mellé odaírjuk azt is, hogy az hányszor szerepelt a bemeneti értékek között! A feladat megoldásához használjunk egy kettős sort! Specifikáció A feladat megoldását két szakaszra bontjuk. Először a bemenő értékek sorozatát alakítjuk át úgy, hogy az elején legyenek a negatív számok, a végén pedig a többi. (Kényelmesebb a specifikációban a bemenő sorozatot egy felsoroló segítségével elérni: ezt jelöli a cin.) Az első szakasz eredményeként létrejött t sorozat elemeit a második szakaszban bejárjuk, és minden elemére megszámoljuk, hogy az hányszor szerepel ebben a sorozatban. Ehhez a t sorozat kétszintű bejárására van szükség, hiszen az elemek bejárása közben minden elemre egy újabb bejárással kell a számlálást megvalósítani. A = ( cin : enor(z), cout : Z* ) Ef = ( cin =cin ) Uf = ( t ( e ) ( e ) e cin' e cin' e 0 e 0 cout ( e, 1 ) ) e t d d t e A feladat megoldásában szereplő t sorozatot egy speciális tárolóként képzeljük el. Ennek, túl azon, hogy egész számok tárolására képes, vagy az elejére vagy a végére illeszthető be könnyen újabb érték. Így egyszerűen megoldhatjuk azt, hogy a bementről érkező negatív egészeket előre, a többit a sorozat végére fűzzük. Egy olyan tárolót, amelyet egy sorozat reprezentál, és amelynek a végeihez lehet elemeket hozzáfűzni, és csak onnan lehet elemet kivenni, kettős sornak nevezzük. Erre az alábbi műveleteket vezetjük be: 586

587 Loext() : egy elemi érték berakása a sor elejére Lopop() : egy elemi érték levétele a sor elejéről Hiext() : egy elemi érték berakása a sor végére Hipop() : egy elemi érték levétele a sor végéről b:biqueue(z) b.loext(e) b.lopop() b.hipop() b.hiext(e) t : Z* t := <e, t> t := <t, e> t := <t 2,,t t > e := t 1 t := <t 1,, t t 1 > e := t t A kettős sor elemeinek bejárásánál szükségünk lesz egy olyan felsoroló objektumra, amely rendelkezik a szokásos bejáró műveletekkel. Esetünkben ez az alábbiakat jelenti: First() : a kettős sor elejére áll Next() : a kettős sor következő elemére áll End() : jelzi, hogy a kettős sor végére értünk-e Current() : az aktuális értéket adja vissza A felsoroló objektumot a kettős sor speciális művelete, a CreateEnumerator() hozza majd létre. Felvethető az a kérdés, hogy miért akarunk külön felsoroló objektumokat létrehozni, miért nem elég, ha a bejáró műveleteket közvetlenül a kettős sor műveletei közé vesszük fel. Ha így tennénk, akkor egyszerre csak egy bejárást tudnánk egy soron végezni. Márpedig a mi feladatunk az, hogy a sor elemeinek kiíratásához indított bejárás közben minden elemnél megálljunk, és a kívánt számlálás elvégzéséhez egy újabb bejárást is indítsunk. Annak pedig, hogy egy tárolón egy időben több bejárást indíthassunk, az a feltétele, hogy tetszőleges számú felsoroló objektumot lehessen létrehozni és használni. 587

588 Absztrakt program A megoldó programnak két szintje lesz. A felső szinten létrehozunk egy kettős sort, és az előírt módon feltöltjük a Loext() és Hiext() műveleteinek segítségével. Ezután létrehozunk a kettős sorhoz több felsoroló objektumot és azok First(), Next(), End() és Current() műveleteire támaszkodva elvégezzük az elemeknek a számlálásokat is magába foglaló kiíratását. A megoldás tehát két ciklus szekvenciája lesz. Az elsőben a C++ nyelvben megszokott jelölésekkel hivatkozunk a beolvasásra. A feltétel azt jelöli, hogy a legutolsó olvasás sikeres volt, van még feldolgozatlan bemeneti érték. cin.fail() cin >> e cin.fail() e<0 b.loext(e) b.hiext(e) cin >> e A második ciklus összetettebb. A külső ciklus egy felsorolót használ, és a beágyazott számlálás is minden végrehajtása egy-egy újabbat. Alkalmazzuk a C++ nyelvben megszokott jelölés a kiírásra (cout << e << db). it1 = b.createenumerator() it1.first() it1.end() e:=it1.current() db := előfordul(e) db := előfordul(e) it2 = b.createenumerator() it2.first();db:=0 it2.end() it2.current()=e cout << e << db db:=db+1 SKIP 588

589 it1.next() it2.next() A megoldás alsó szintje a kettős sor típusának és a kettős sor felsorolója típusának megvalósítását tartalmazza. Mindkettőre egy-egy osztályt hozunk létre. A kettős sort egy fejelem nélküli kétirányú láncolt listával fogjuk reprezentálni. Két pointerrel hivatkozunk majd erre a listára: first: lista első elemére mutat (üres lista esetén nil) last: lista utolsó elemére mutat (üres lista esetén nil) A kettős sor műveleteit a láncolt lista elejére és végére történő beszúrás, és onnan való törlés implementálja. Ezen műveletek mellé egy speciális metódust is felveszünk: a CreateEnumerator() segítségével tudunk majd a kettős sorhoz egy új felsoroló objektumot létrehozni. A felsoroló objektumot két pointerrel reprezentáljuk. Az egyik arra a kettős sorra mutat, amelyik elemeit felsoroljuk, a másik a kettős sort reprezentáló láncolt lista azon listaelemére mutat majd, amelyik a felsorolás során érintett aktuális elem. Implementálás A program komponens szerkezete 589

590 main.cpp main() biqueue.h-biqueue.cpp class BiQueue BiQueue() ~BiQueue() Loext() Lopop() Hiext() Hipop() CreateEnumerator() biqueue.h class Enumerator Enumerator() ~Enumerator() First() Next() Current() End() ábra. Komponens szerkezet A main függvény tartalmazza az absztrakt megoldás felső szintjét. Ez a program közvetlenül hivatkozik a BiQueue osztályra és annak belső osztályaként definiált Enumerator osztályra, ezért a main.cpp állományba be kell inklúdolni a biqueue.h állományt. Főprogram kódolása A főprogramban létrehozunk egy üres kettős sort, majd a szabványos bementről érkező számokat előjelüktől függően belerakjuk a sorba. A kódolásnál ismét kihasználjuk, hogy a cin >> i hibás működés esetén hamis értéket ad vissza. BiQueue x; int i; while(cin >> i){ if (i>0) x.hiext(i); else x.loext(i); 590

591 A beolvasás után bejárjuk a sort, és kiírjuk az elemeit a szabványos kimenetre, közben minden elemre megszámoljuk, hányszor van benne a sorban. Ehhez egyidejűleg két felsorolót használunk. A BiQueue osztályba ágyazott Enumerator osztályra a BiQueue::Enumerator-ral hivatkozhatunk. BiQueue::Enumerator it1 = x.createenumerator(); for(it1.first();!it1.end(); it1.next()){ i = it1.current(); int s = 0; BiQueue::Enumerator it2 = x.createenumerator (); for(it2.first();!it2.end(); it2.next()){ if (it2.current() == i) ++s; cout << i << " előfordulásainak száma: " << s << endl; return 0; A kettős sor osztálya Az Exceptions felsorolt típus azt az értéket tartalmazza, amelyet a kettős sor hibás felhasználás esetén kivételként dob. Jelen esetben ez akkor 591

592 következik be, ha egy üres sorból ki akarunk venni egy értéket (ld. Lopop() és Hipop() műveleteknél). class BiQueue{ public: enum Exceptions{EMPTYSEQ; BiQueue(): first(null),last(null){ ~BiQueue(); void Loext(int e); int Lopop(); void Hiext(int e); int Hipop(); BiQueue(const BiQueue&); BiQueue& operator=(const BiQueue&); A konstruktort inline módon definiáltuk. Ez egy üres kettős sort, azaz egy üres láncolt listát hoz létre úgy, hogy a két privát pointert NULL-ra állítja. Mind a másoló konstruktort, mind az értékadás operátort újradefiniáljuk. private: struct Node{ int val; Node *next; 592

593 Node *prev; Node(int c, Node *n, Node *p): val(c), next(n), prev(p){; ; Node *first; Node *last; A privát részben adjuk meg a listaelemek típusát. Ez egy három részből (érték és két pointer) álló struktúra (Node), amelynek konstruktorával hozhatunk létre egy új listaelemet. E struktúra ismeretében definiáljuk a láncolt lista legelső illetve legutolsó listaelemére mutató first és last pointereket. Ez a két pointer lényegében a kettős sor reprezentációja. Ezzel a BiQueue osztály definícióját még nem fejeztük be. Hiányzik a CreateEnumerator() metódus deklarációja, de ehhez előbb szükség van az Enumerator osztály definíciójára. Felsoroló a kettős sorhoz Egy kettős sor felsorolását megvalósító osztályt (class Enumerator) a BiQueue osztály beágyazott publikus osztályaként definiáljuk. Ez egyrészt egyértelművé teszi, hogy itt a BiQueue osztály felsorolójáról lesz szó, másrészt az Enumerator osztály metódusai hivatkozhatnak a BiQueue osztály privát elemeire is. Egy felsorolót elsősorban az a current pointer reprezentálja, amelyet a kettős sor láncolt listáján tudunk végigvezetni és a bejárás során mindig az aktuális listaelemre mutat. Ezen kívül a felsoroló magára a bejárni kívánt kettős sorra is hivatkozik egy pointer segítségével (bq). Az Enumerator osztály metódusai: First(), Next(), Current(), End(). Ezeknek a műveleteknek az implementációja igen egyszerű (éppen ezért inline módon adjuk meg), lényegében a kettős sort reprezentáló láncolt listával és a current pointerrel operálnak. A First() ráállítja a current pointert a lista első elemére, a Next() a következőre, az End() akkor ad igazat, ha a 593

594 current értéke már NULL (azaz lefutott a listáról), a Current() pedig a current pointer által mutatott listaelem értékét adja vissza. class Enumerator{ public: Enumerator(BiQueue *p): bq(p),current(null){; int Current()const {return current->val; void First() {current = bq->first; bool End() const {return NULL == current; private: void Next() {current = current->next; BiQueue *bq; Node *current; ; A felsoroló nem végez dinamikus helyfoglalást, ezért az alapértelmezett másoló konstruktor és értékadás operátor itt megfelelő. (Egyébként sem jellemző e metódusok használata.) Alternatív megvalósítás lehet az, hogy a First() művelet feladatát a konstruktor látja el, a Next()-et pedig az operator++() felüldefiniálásával valósítjuk meg. Enumerator operator++(int){ // it++ Enumerator it = *this; current = current->next; 594

595 return it; Enumerator& operator++(){ // ++it current = current->next; return *this; Egy BiQueue::Enumerator it(&x) utasítás segítségével hozhatunk létre egy it nevű felsorolót az x kettős sorhoz. Ezt a műveletet a kettős sor korábban már beígért CreateEnumerator() metódusával is elvégezhetjük. Térjünk tehát vissza a kettős sor osztályának definíciójához. A kétirányú sor osztálydefiníciójának folytatása Most már felvehetjük a BiQueue osztályba a CreateEnumerator() publikus metódust, amelynek implementációját inline módon adjuk meg. Enumerator CreateEnumerator() { return Enumerator(this); Az BiQueue osztály definíciója ezzel el is készült. Hátra van még a korábban deklarált műveleteknek a megvalósítása. A destruktor felszabadítja a kettős sort ábrázoló láncolt listát. Itt is látható, hogy míg a konstruktor egyáltalán nem végez memóriafoglalást, addig a destruktor számos listaelemet felszabadíthat, hiszen a kettős sor használata során a Loext() és Hiext() műveletek dinamikusan hozzák létre a listaelemeket. BiQueue::~BiQueue(){ Node *p, *q; q = first; 595

596 while( q!= NULL){ p = q; q = q->next; delete p; A másoló konstruktor egy p pointert vezet végig a lemásolandó kettős soron (feltéve, hogy az nem üres), és az új kettős sor listájának felépítéséhez egy q pointert használ. BiQueue::BiQueue(const BiQueue &b){ if(null == b.first){ first = last = NULL; else{ Node *q = new Node(b.first->val,NULL,NULL); first = q; for(node *p=b.first->next; p!= NULL;p=p->next){ q = new Node(p->val,NULL,q); q->prev->next = q; last = q; 596

597 Az értékadás operátort az alábbi séma mintájára definiáljuk. BiQueue& BiQueue::operator=(const BiQueue &s){ if(&s == this) return *this; // destruktor // másoló konstruktor return *this; Miután megvizsgálja, hogy az alapértelmezés szerinti (értékadás baloldali) objektuma különbözik-e az értékül adandó objektumtól, előbb a destruktornak megfelelően felszabadítja az alapértelmezés szerinti objektum memóriafoglalásait, majd a másoló konstuktorhoz hasonlóan létrehozza az új kettős sort. BiQueue& BiQueue::operator=(const BiQueue &s){ if(&s == this) return *this; Node *p = first; while(p!= NULL){ Node *q = p->next; delete p; p = q; if(null == s.first){ first = last = NULL; else{ 597

598 Node *q = new Node(s.first->val,NULL,NULL); first = q; for(node *p=s.first->next; p!= NULL;p=p->next){ q = new Node(p->val,NULL,q); q->prev->next = q; last = q; return *this; A Loext() illetve a Hiext() művelet létrehoz egy új listaelemet, kitölti annak értékét, és befűzi a lista elejére illetve a végére, és ennek megfelelően állítjuk a first vagy a last pointert. Amikor a legelső elemet fűzzük be az üres listába, akkor mindkét esetben mindkét pointert állítani kell. void BiQueue::Loext(int e){ Node *p = new Node(e,first,NULL); if(first!= NULL) first->prev = p; first = p; if(null == last) last = p; void BiQueue::Hiext(int e){ 598

599 Node *p = new Node(e,NULL,last); if(last!= NULL) last->next = p; last = p; if(null == first) first = p; A Lopop() művelet EMPTYSEQ kivételt dob, ha a lista üres, egyébként kifűzi a lista legelső elemét, a benne tárolt értéket elmenti, és a listaelemet felszabadítja. Ha a lista eredetileg egyelemű volt, akkor az utolsó elemre mutató last pointert NULL-ra kell állítani. A Hipop() a Lopop() duálisa. int BiQueue::Lopop(){ if(null == first) throw EMPTYSEQ; int e = first->val; Node *p = first; first = first->next; delete p; if(first!= NULL) first->prev = NULL; else last = NULL; return e; int BiQueue::Hipop(){ if(null == last)throw EMPTYSEQ; int e = last->val; Node *p = last; 599

600 last = last->prev; delete p; if(last!= NULL) last->next = NULL; else first = NULL; return e; Elem törlése bejárás közben Egy problémával érdemes még foglalkoznunk annak ellenére, hogy a fenti alkalmazásban ennek nincs szerepe. A kettős sor elemeinek felsorolása ugyanis elromolhat, ha bejárás során a sorból törlünk egy olyan elemet, amelyre éppen a felsoroló current pointere hivatkozik. A probléma elkerülésére több megoldás is elképzelhető: Teljes kizárás: (Ezt a változatot építettük be az alább látható teljes programba.) A kettős sor reprezentációjában nyilvántartjuk a létrehozott felsorolók számát (ez új kettős sornál kezdetben nulla), amit a felsoroló konstruktora növel, destruktora csökkent, és ha ez a számláló nem nulla, akkor a kettős sor Lopop() és Hipop() törlőműveletei egy speciális kivételt dobnak. Elemszintű kizárás: A törlő műveletek csak akkor dobnak kivételt, ha olyan listaelemet törölnének, amelyre valamelyik felsoroló éppen hivatkozik. Ehhez nyilván kell tartani a kettős sor reprezentációjában a létrehozott felsorolókat, és végig kell vizsgálni azokat a törlés előtt. Törlés késleltetés: Ha a törlendő elemre éppen egy felsoroló mutat, akkor annak törlését elhalasztjuk. Ehhez meg kell jelölni a törlendő elemet, és amikor egyik felsoroló sem mutat erről listaelemre, akkor törölhetjük. A megvalósításhoz itt is nyilván kell tartani a kettős sor reprezentációjában a létrehozott 600

601 felsorolókat, a listaelemeket pedig ki kell egészíteni egy törlést jelző mezővel. Egy törlés újbóli kísérletét a felsorolók Next() műveletének végrehajtásához köthetjük. Tesztelés Az alapfeladat érvényes fekete doboz tesztelését kipróbálhatjuk nulla darab bemenettel, majd egy darabbal, majd csupa különböző értékkel (legyenek köztük negatív és nem negatív értékek is), csupa azonos értékkel, végül egy általános esettel. Az érvénytelen tesztesetek a kivétel dobások kikényszerítésére irányulnak. A főprogram azonban nem ad lehetőséget az EMPTYSTACK kivétel dobására, hiszen nem törlünk. Érvénytelen teszteset az is, amikor valamelyik new utasítás bad_alloc kivételt dob. Ennek kikényszerítésére sem alkalmas a főprogramunk, az előző feladatnál látott tesztprogramot kellene most is elkészíteni. A BiQueue osztály komponens tesztjéhez most is egy menüválasztós tesztprogramot kell készíteni. A műveletek variációs tesztjéhez a teljesség igénye nélkül néhány példa: 1. Ha üres, vagy bármilyen más sorra többször alkalmazzuk a Hiext() műveletet, majd ugyanannyiszor a Lopop() műveletet, akkor visszakapjuk a kiindulási sort. 2. A Hiext(), Loext(), Hipop(), Lopop() művelet bármelyike egymás után nem hajtható végre akárhányszor ugyanazzal az eredménnyel. 3. Kezdetben üres sorra végrehajtott Hiext(1), Hiext(2), First(), Next(), Hipop() után törlés bejárás közben kivétel keletkezik. 601

602 Teljes program main.cpp: #include <iostream> #include "biqueue.h" using namespace std; int main() { BiQueue x; int i; while(cin >> i){ if (i>0) x.hiext(i); else x.loext(i); BiQueue:: Enumerator it1 = x.createenumerator(); for(it1.first();!it1.end(); it1.next()){ i = it1.current(); int s = 0; BiQueue:: Enumerator it2 = x.createenumerator (); for(it2.first();!it2.end(); it2.next()){ if (it2.current() == i) ++s; 602

603 cout << i << " előfordulásainak száma: " << s << endl; return 0; 603

604 biqueue.h: #ifndef BIQUEUE_H #define BIQUEUE_H #include <memory> class BiQueue{ public: enum Exceptions{EMPTYSEQ, UNDERTRAVERSAL; BiQueue(): first(null),last(null),enumeratorcount (0){ BiQueue(const BiQueue&); BiQueue& operator=(const BiQueue&); ~BiQueue(); void Loext(int e); int Lopop(); void Hiext(int e); int Hipop(); private: struct Node{ int val; Node *next; Node *prev; Node(int c, Node *n, Node *p): val(c), next(n), prev(p){; 604

605 ; Node *first; Node *last; int enumeratorcount; public: class Enumerator{ public: Enumerator(BiQueue *p):bq(p),current(null) {++(bq->enumeratorcount); ~Enumerator(){--(bq->enumeratorCount); int Current()const {return current->val; void First() {current = bq->first; bool End() const {return NULL == current; void Next() {current = current->next; private: BiQueue *bq; Node *current; ; Enumerator CreateEnumerator() {return Enumerator(this); ; #endif biqueue.cpp: 605

606 #include "biqueue.h" using namespace std; BiQueue::~BiQueue(){ Node *p, *q; q = first; while( q!= NULL){ p = q; q = q->next; delete p; BiQueue::BiQueue(const BiQueue &s){ if(null == s.first)first = last = NULL; else{ Node *q = new Node(s.first->val,NULL,NULL); first = q; for(node *p=s.first->next; p!= NULL;p=p->next){ q = new Node(p->val,NULL,q); q->prev->next = q; last = q; 606

607 BiQueue& BiQueue::operator=(const BiQueue &s){ if(&s == this) return *this; Node *p = first; while(p!= NULL){ Node *q = p->next; delete p; p = q; if(null == s.first) first = last = NULL; else{ Node *q = new Node(s.first->val,NULL,NULL); first = q; for(node *p=s.first->next; p!= NULL;p=p->next){ q = new Node(p->val,NULL,q); q->prev->next = q; last = q; return *this; 607

608 void BiQueue::Loext(int e){ Node *p = new Node(e,first,NULL); if(first!= NULL) first->prev = p; first = p; if(null == last) last = p; int BiQueue::Lopop(){ if(enumeratorcount!= 0) throw UNDERTRAVERSAL; if(null == first) throw EMPTYSEQ; int e = first->val; Node *p = first; first = first->next; delete p; if(first!= NULL) first->prev = NULL; else last = NULL; return e; void BiQueue::Hiext(int e){ Node *p = new Node(e,NULL,last); if(last!= NULL) last->next = p; last = p; if(null == first) first = p; int BiQueue::Hipop(){ 608

609 if(enumeratorcount!= 0) throw UNDERTRAVERSAL; if(null == last)throw EMPTYSEQ; int e = last->val; Node *p = last; last = last->prev; delete p; if(last!= NULL) last->next = NULL; else first = NULL; return e; 609

610 C++ kislexikon pointer dinamikus helyfoglalás és törlés int* p; int* p = new int; int *t = new int[10]; delete p; delete[] t; listaelem definiálása struct Node { int value; Node *next; Node(int i=0, Node *q=null) :value(i), next(q){ ; listaelem létrehozása Node *p = new Node(); Node *p = new Node(0); Node *p = new Node(0,NULL); beszúrás // Node *u egy létező listaelem címével Node *p = new Node(23,u->next); u->next = p; törlés listaelem mögül // Node *u egy létező listaelem címével Node *p = u->next; if(p!=null){ u->next = p->next; delete p; 610

611 lista felépítése lista lebontása Fejelemes Node *h = new Node(); Node *u = h; for(int i=1;i<n;i++){ Node *p = new Node(i); u->next=p; u = p; while(h!=null){ Node *p = h; h = p->next; delete p; Fejelem nélküli Node *h = new Node(1); Node *u = h; for(int i=2;i<n;i++){ Node *p = new Node(i); u->next=p; u = p; értékadás operátor O& O::operator=(const O &s){ if(&s == this) return *this; // destruktor törzse // másoló konstruktor törzse return *this; 611

612 14. Objektum-orientált kód-újrafelhasználási technikák Egy program költségét több szempont alapján határozhatjuk meg: ilyenek a futási idő, a memória igény, a programkód előállítási költsége, a hibajavítás és karbantartás költsége. Például nemcsak elegáns, de az előállítási és a hibajavítási költségen is javít, ha a programkód nem tartalmaz ismétlődő részeket, és egy többször is felhasználandó kódrészt csak egyszer írunk le a kódban, majd azokon a helyeken, ahol szükség van rá, csak felhasználjuk. Ha ráadásul egy ilyen kódrész önálló csomagban (komponensben) helyezkedik el, akkor más alkalmazásokban könnyen újra fel tudjuk majd használni. A kód ilyen újrafelhasználását szolgálják a korábban már ismertetett alprogramok, amelyek egy-egy részprogram sokszoros felhasználására adnak lehetőséget. Ide sorolható az is, amikor azonos tulajdonságú objektumok közös leírására osztályokat definiálunk. Ebben a fejezetben olyan további nyelvi eszközöket mutatunk be, amelyekkel egy osztály definíciójában leírt kódot újra fel lehet használni, de úgy, hogy lehetőségünk legyen az újrafelhasznált kódon némiképp változtatni, hozzászabni azt a konkrét alkalmazás céljaihoz. A származtatás az újrafelhasználás objektum orientált nyelvi eszköze. Ennek keretében egy osztályt az utódosztályt egy már létező másik osztály az ősosztály mintájára, az ősosztályhoz hasonlóra definiálhatunk úgy, hogy az utódosztály megkapja, örökli az ősosztály tulajdonságait, azaz rendelkezni fog az ősosztály adattagjaival és metódusaival, de az örökölt tagokon kívül felruházhatjuk egyéb tulajdonságokkal is: kiegészíthetjük újabb adattagokkal és metódusokkal, felüldefiniálhatjuk az ősosztály bizonyos metódusait. Az újrafelhasználás másik nyelvi eszköze a sablon (template, generic) példányosítása. Most elsősorban osztály-sablonokról lesz szó, amikor egy osztály definícióját úgy adjuk meg, hogy abban bizonyos elemeket nem konkretizálunk, csak később megadandó paraméterekkel jelölünk. Ilyen lehet például egy olyan verem-típust megvalósító osztály, ahol a verembe betenni kívánt elemek típusa még ismeretlen, azt egy paraméter helyettesíti. Amikor egy ilyen osztályt fel akarunk használni (objektumot készítünk a mintájára) 612

613 akkor először ezeket a sablon-paramétereket kell konkrét elemekkel helyettesíteni, azaz az osztály-sablonból létre kell hoznunk, példányosítanunk kell egy konkrét osztályt. Mindkét nyelvi eszköz azt támogatja, hogy közös tulajdonsággal rendelkező osztályok használata esetén, az osztályok hasonló elemeit csak egyszer kelljen megadni, leírni, és azokat újra és újra felhasználni. Implementációs stratégia Az, hogy a programunkban használt osztályok előállításához kell-e ősosztályokat és/vagy osztály-sablonokat használni, sokszor már a tervezés során kiderül, hiszen már ekkor felismerhetjük, ha egy feladat megoldásában részt vevő különböző típusok hasonlítanak egymásra, és kísérletet tehetünk a hasonló tulajdonságok közös leírására. Előfordul azonban az is, hogy minderre csak az implementáció során figyelünk fel. Ha például hasonló tulajdonságokkal rendelkező típusok osztályait kell elkészítenünk, akkor elvonatkoztatva az azokat megkülönböztető részletektől egy általános osztályt kapunk. Az általánosítással összefogott osztályok közös adattagjai az általános osztály adattagjai lesznek. Ugyanez a helyzet a teljesen azonos metódusokkal: elég őket egyszer az általános osztályban megadni. Ha a konkrét osztályokat ebből az általános osztályból, mint ősosztályból származtatjuk, akkor azok rendelkezni fognak az általános osztályban definiált adattagokkal és metódusokkal, de ezen kívül egyéb tulajdonságokkal is felruházhatók, azaz specializálhatók. Más szóval kiegészíthetők további adattagokkal és metódusokkal. (A specializálás fogalmán ugyan túlmutat, de elérhetjük azt is, hogy bizonyos adattagok és metódusok ne öröklődjenek az általános osztályból.) A specializáció következménye, hogy egy utódosztálybeli objektum mindig elfogadható az ősosztályának objektumaként; azaz egy ősosztály típusú változónak értékül lehet adni annak utódosztályához létrehozott objektumot. A specializálás során lehetőségünk van egy öröklött metódus működésén változtatni. Így olyan osztályt származtathatunk, amely rendelkezik ugyan az ősosztály egy bizonyos metódusával, annak neve, 613

614 paramétereinek és visszatérésének típusa, azaz a deklarációja megegyezik az ősosztálybelivel, de a működése eltér attól. Ehhez az utódosztályban az öröklött metódust (pontosabban annak törzsét) kell felüldefiniálni vagy újradefiniálni. E két fogalom nem szinonimája egymásnak, lényeges különbség van közöttük, amely akkor mutatkozik meg, amikor egy ősosztály típusú változónak értékül adjuk annak utódosztályához létrehozott objektumot és meghívjuk rá a módosított metódust. Az újradefiniált metódusnak csak annyi kapcsolata van az ősosztálybeli megfelelőjével, hogy a metódus feje ugyanaz. Az újnak, amelyik felülírja a régit, nincs semmi köze a régihez. Ha egy ősosztály típusú változónak értékül adjuk annak utódosztályához létrehozott objektumot és meghívjuk rá ezt a metódust, akkor az ősosztálybeli változat fog végrehajtódni, mert semmi nem indokolja, hogy az ősosztálybeli változó számára látható metódus helyett egy másik (azonos nevű és típusú) metódus hívódjon meg. Ezt a jelenséget hívják statikus kötésnek. Az elnevezés arra utal, hogy már fordítási időben eldől, hogy a változóhoz rendelt (kötött) metódus melyik az azonos nevű és típusú metódusok közül. A felüldefiniált metódus (amelyet virtuális metódusként is szokás emlegetni) ellenben szoros kapcsolatban marad az eredeti, az ősosztálybeli megfelelőjével. Az eredeti metódus ismeri önmaga felüldefiniált változatait, ezért ha egy ősosztály típusú változóra hívják meg, akkor mindig az a véltozat fog meghívódni, amelyik a változónak értékül adott objektumra (ez lehet egy utódosztály objektuma) érvényes. Ezt a jelenséget hívják polimorfizmusnak (többalakúság) vagy dinamikus kötésnek. A második elnevezés arra utal, hogy csak futási időben, azaz dinamikus módon lehet el dönteni, hogy egy ősosztálybeli változó az adott pillanatban milyen osztályú objektumot tárol, és csak ennek ismeretében derül ki, hogy a kódban a változóhoz rendelt (kötött) metódushívás valójában melyik metódus-változat működését váltja ki. A kód-újrafelhasználás megvalósításához a dinamikus kötés, azaz a felüldefiniálás igen erős eszközt ad a kezünkbe. Az általánosítás során, amikor több olyan osztálynak készítjük el az ősosztályát, amelyek rendelkeznek egy azonos fejű (nevű és típusú) metódussal, az alábbiak szerint járunk el. 614

615 1. Ha a vizsgált metódusok mindegyike ugyanazt csinálja az utódosztályokban, akkor ezeket az utódosztályban nem definiáljuk, elég egyetlen egyszer megadni ezt az ősosztályban, amelyet az utódosztályok örökölnek, felüldefiniálni nem kell. 2. Ha a vizsgált metódusok az egyes utódosztályokban eltérően működnek, akkor definiáljuk egyrészt az ősosztályban egy ott adekvát működéssel, de az utódosztályokban külön-külön felüldefiniáljuk. (Ha nincs szükség arra, hogy az ősosztályhoz létrehozzunk objektumokat, akkor elég a metódusnak az ősosztályban csak a deklarációját megadni. A metódus ilyenkor az ősosztályban absztrakt lesz.) 3. Ha a vizsgált metódusok működése tartalmaz közös részeket, akkor ezeket a metódusokat a konkrét osztályokban ne definiáljuk, az ősosztályban pedig úgy, hogy azokon a helyeken, ahol az eltérések vannak egy-egy olyan ősosztálybeli metódust hívjunk meg, amellyeket a származtatott osztályokban a kívánt módon felüldefiniálunk. A származtatás lehet közvetett illetve többszörös. Közvetett származtatásról akkor beszélünk, amikor a C osztály közvetlenül a B osztály leszármazottja, a B pedig az A osztályé, és ennél fogva a C osztály közvetve az A osztályból származik. A többszörös származtatás fogalma arra utal, hogy egy osztálynak egyszerre több közvetlen őse is lehet. Van úgy, hogy az általános osztályhoz is létrehozunk objektumot (ilyenkor természetesen minden metódusát definiálni kell), de sokszor az általános osztály csak arra szolgál, hogy abból más osztályokat származtassunk, és soha nem hozunk létre belőle objektumokat. Ilyenor nem kell azokat a metódusait definiálni, amelyeket a leszármazott osztályok úgyis definiálnak, elegendő csak deklarálni ezeket. Az ilyen absztrakt metódusokat tartalmazó általános osztályt absztrakt osztálynak hívjuk. 2 2 Megjegyezzük, hogy az absztrakt osztály és az absztrakt típus fogalma között nincs szoros összefüggés: az absztrakt osztály nem az absztrakt típus megvalósítása. Az absztrakt típust egy adat jellemzésére szolgáló fogalomként vezettük be, amelyet részben vagy nem végleges formában valósítottunk meg. Az absztrakt osztály viszont a kód-újrafelhasználás 615

616 Az általánosítás és specializálás jól alkalmazható az alternatív típus megvalósításánál. (Ez az a típusszerkezet, amelyre a programozási nyelvek általában nem biztosítanak közvetlen nyelvi eszközt ellentétben a rekordvagy a sorozatszerkezettel.) Már tervezéskor kiderülhet, hogy egy tárolóba eltérő típusú értékeket akarunk tárolni. Ilyenkor a tároló elemi típusa egy alternatív típus lesz. Ennek megvalósításhoz az alkotó típusoknak az osztályait kell általánosítani, és az így kapott ősosztály lesz a tároló elemi típusa. Amikor néhány osztály között csak annyi különbség van, hogy eltér bizonyos adattagjainak vagy metódusaik paramétereinek, esetleg azok visszatérési értékének típusa, akkor azokat osztály-sablonként érdemes általánosítani. Ebben az eltérő típusok helyén egy-egy típust helyettesítő paraméter fog állni. (Egy paraméter nemcsak típusokat helyettesíthet, hanem konkrét értéket is.) Egy ilyen osztály-sablonnal leírt általános osztályból egy konkrét osztály nem származtatással, hanem a paramétereinek konkrét értékekkel (ez lehet egy konkét típus vagy egy konstans érték) történő behelyettesítésével készíthető el, más szóval példányosodik. 3 Összességében elmondhatjuk, hogy egy általános osztályt konkrét típusok absztrakciója során előállt általános típus leírására készítjük. Ennek jellemzője, hogy lehetnek nem-definiált (absztrakt) metódusai illetve sablonparaméterei. Az általános osztály specializációja során konkrét elemeket adunk az osztályhoz. Ez történhet úgy, hogy a származtatás során egy metódusnak felüldefiniáljuk működését és az osztályt kiegészítjük egyéb tagokkal, vagy példányosítással megadjuk a sablon-paramétereit. Előfordul, érdekében bevezetett olyan nyelvi elem, amely egy vagy több osztály őse, közös tulajdonságaik (adattagjaik, metódusaik) hordozója, de objektum nem hozható létre belőle. 3 Erre a tevékenységre ugyanaz az elnevezés terjedt el, mint amit az objektumok létrehozására használnak. Ez az oka, hogy ebben a könyvben példányosításon azt értjük, amikor egy osztály-sablonból a sablonparaméterek megadása mellett új osztályt hozunk létre. 616

617 hogy mindkét technikát egyszerre alkalmazzuk, de az is, hogy ugyanaz a konkrét információ alternatív módon mindkét technikával hozzáadható egy általános osztályhoz. Ilyenkor érdemes inkább a sablont használni, mivel a sablon-példányosítás fordítási időben hajtódik végre. 617

618 Nyelvi elemek A fenti implementációs elveket nyelvi szinten a származtatás és a sablonosítás támogatja. Származtatásnál egy utódosztály definíciójában, a definíció fejében kell jelölni a közvetlen ősosztályt. (Például C++ és C# nyelven erre a kettőspont, Java nyelven az extends kulcsszó szolgál). C++ nyelven egy utódosztályhoz egyszerre több közvetlen ősosztály is megadható. A tisztán objektum-orientált nyelvekben a többszörös öröklődés csak azzal a feltétellel lehetséges, hogy az ősosztályoknak egy kivételével, úgynevezett interfészeknek kell lenniük. Az objektum-orientált nyelvekben egy speciális kulcsszóval (interface) jelzett teljesen absztrakt osztályokat hívják interfésznek. Járjuk körbe ezt a meghatározást. Implementációs szempontból absztrakt osztály az, amelyhez nem akarunk létrehozni objektumokat, nyelvi szempontból viszont az, amelyhez nem is tudunk. Ezt megakadályozhatja például egy speciális kulcsszóval történő megjelölés (pl. a C#, Java nyelveken ez a kulcsszó az abstract), de absztrakt osztály az is, amelynek hiányos definíciója, azaz valamelyik (akár az összes) metódusának a törzse hiányzik, más szóval vannak absztrakt metódusai. Absztrakttá lehet tenni egy osztályt úgy is, hogy csak privát konstruktorai vannak. Teljesen absztrakt osztály (tehát interfész) az, amelynek minden metódusa absztrakt és nincsenek adattagjai. Teljesen absztrakt osztályokat a C++ nyelven is készíthetünk, de nincs külön kulcsszó sem az absztrakt osztály, sem az interfész jelölésére. Egy interfészből történő származtatáskor az interfész összes metódusát felül kell definiálni. Erre mondjuk azt, hogy implementáljuk az interfészt. Az utódosztályra az ősosztály privát adattagjai és metódusai nem öröklődnek. A védett (protected) minősítésű tagok az adott osztályra nézve viszont ugyanúgy viselkednek, mint a privátok, ugyanakkor öröklődnek az utódosztályra. Ezért érdemes a privát minősítéseket egy osztályban inkább védettre cserélni, hacsak kifejezetten nem az a szándékunk, hogy egy esetleges származtatásnál az adott tagot ne lehessen örökíteni. C++ nyelven a származtatásnak többféle módja van. Ezek közül mi a publikus származtatást fogjuk használni ezt explicit módon jelölni kell amikor az ősosztály tagjainak láthatósága (public, protected) is öröklődik, 618

619 azaz megmarad az utódosztályban. A tisztán objektum-orientált nyelvekben többnyire csak ilyen publikus származtatással találkozhatunk. Csak a tisztán objektum-orientált nyelvekben van lehetőség arra, hogy bizonyos osztályokra megtiltsuk, hogy azokból más osztályokat származtassunk. Az ilyen, a származtatási láncok legalján szereplő, úgynevezett végső osztályokat speciális kulcsszavak jelzik (sealed, final). Azokat a metódusokat, amelyeknek megengedjük a felüldefiniálásukat virtuálisként (virtual) kell megjelölni. Bizonyos nyelvekben a felüldefiniálás (override) vagy újradefiniálás (new) tényét külön is jelölni kell az utódosztályban, de C++ nyelven erre nincs lehetőség. Ha egy ősosztálybeli metódus virtuális, akkor csak felüldefiniálni lehet és ilyenkor a leszármazott metódus is virtuális lesz. Ha az ősosztálybeli metódus nem virtuális, akkor az utódosztályban csak az újradefiniálásáról lehet beszélni. Általában az ősosztály metódusait (destruktorát is) a konstruktorai kivételével virtuálisként adjuk meg. A virtuális metódusok teremtik meg a polimorfizmus jelenségét. Nyelvi szempontból egy ilyen metódus (ne felejtsük, hogy a virtuális metódus felüldefiniáltja is virtuális) meghívásakor nem fordítási időben kötődik (dinamikus kötés) a hívó utasításhoz a hívott metódus kódja. Az ugyanis, hogy annak az objektumnak, amire a metódust meghívtuk mi a típusa, azaz melyik osztálynak (az ősosztálynak vagy annak egy utódosztályának) példánya, sokszor csak futás közben deríthető ki. A polimorfizmus jelentősége akkor mutatkozik meg, amikor programunkban egy ősosztály típusú változónak értékül adjuk egy utódosztály objektumát. Ha egy ilyen változóra meghívunk egy virtuális metódust, akkor a tisztán objektum-orientált nyelvekben az utódosztály metódusa hajtódik végre. C++ nyelven ilyen helyzet úgy teremthető, ha egy ősosztály típusú pointerváltozónak adjuk az utódosztálya egy példányának címét, és erre a pointerváltozóra kezdeményezzük egy virtuális metódus hívását. A C++ nyelven arra is lehetőség van, hogy egy ősosztály típusú változónak (nem pointerváltozónak) közvetlenül adjuk értékül az utódosztály egy objektumát (azaz látszólag ugyanazt tesszük, mint a tisztán objektumorientált nyelveknél). Az ilyen változóra történő virtuális metódus meghívásakor azonban nincs dinamikus kötés, az ősosztálybeli metódus fog 619

620 lefutni. (A látszat ellenére a tisztán objektum-orientált nyelvek és a C++ nyelv dinamikus kötése között valójában nincs különbség. A tisztán objektumorientált nyelvekben ugyanis csak látszólag nincsenek pointerváltozók, de egy osztálynak egy változója lényegében pointerváltozó. Erre utal az is, hogy az objektumok létrehozása a new utasítással történik.) Amikor a származtatás során egy ősosztálybeli metódust az utódosztályban újra- vagy felüldefiniálunk, akkor az eltakarja az eredeti definíciót. Néha azonban az utódosztályban szükség lehet az ősosztálybeli metódus közvetlen meghívására. Ehhez C++ nyelven az ősosztály nevével történő minősítést (név::) kell használni (Java nyelven: super., C# nyelven: base.). Utódobjektum létrehozásakor először mindig az ősosztály konstruktora, majd az utódosztály konstruktora hajtódik végre. Az objektum megszűnésekor fordított a sorrend: először az utódosztály, majd az ősosztály destruktora fut le. Ne felejtsük el: C++ nyelven, ha egy ősosztály típusú pointerváltozónak egy utódobjektum címét adjuk értékül, akkor az utódosztály destruktora csak akkor fut le, ha az ősosztály destruktora virtuális. A sablonosítás azt jelenti, hogy olyan kódrészt készítünk, amelynek bizonyos elemeit speciális paraméterek helyettesítik, amelyeket a konkrét használat előtt meg kell adni, azaz példányosítani kell. A példányosítás mindig fordítási időben történik. Ebben a fejezetben elsősorban osztálysablonokkal foglalkozunk, de a C++ nyelven lehetőség van függvény-sablonok készítésére is. Tulajdonképpen egy osztály-sablon egy metódusa is függvénysablonnak számít. A sablon-paraméterek többfélék lehetnek. Legtöbbször típusokat helyettesítő úgynevezett típus-paramétereket használunk, ami lehet akár osztály-sablon típusú is, de találkozhatunk értéket jelölő érték-paraméterrel is. Egy kód-sablon paramétereit a kódrészlet (osztály, függvény) fejében kell felsorolni. Ennek szintaxisa a választott nyelvtől függ. C++ nyelven a template < > kifejezés előzi meg az osztály-sablont, ebben soroljuk fel (vesszővel elválasztva) a paramétereket. A paraméterek előtt meg kell adni azok típusát. Ez egy típust helyettesítő paraméter esetén a typename, 620

621 értékeket helyettesítő paraméternél az érték konkrét típusa. Ezt a template < > kifejezést meg kell ismételni az osztályon kívül (tehát nem inline módon) definiált metódusok feje előtt is. A fejrészben a metódus neve előtt nemcsak az osztály-sablon neve kell, hogy álljon minősítésként, hanem a név után kisebb-nagyobb (< >) jelek között az összes paramétert is fel kell sorolni azok típusának megjelölése nélkül. Tulajdonképpen ez az osztály-sablon hivatalos neve. Ha egy metódus visszatérési típusa a saját osztály-sablonja (ilyennel találkozhatunk a másoló konstruktornál), akkor itt is az előbb említett teljes névvel (osztálynév+paraméterek) kell az osztály-sablonra hivatkozni, ellenben a definíció többi részén elég az osztály-sablonnak csak nevét használni a paraméterek felsorolása nélkül. Példányosításkor a kódrészlet azonosítója (osztály-sablon esetén az osztály neve) után kisebb-nagyobb jelek között kell a sablon-paraméterek konkrét értékeit (típusokat illetve konstansokat) felsorolni ugyanabban a sorrendben, mint ahogy azok a definícióban szerepeltek. Az utolsó néhány paraméter rendelkezhet alapértelmezett értékkel, ezeket a példányosításnál nem kell megadni. Egy osztály-sablont is elhelyezhetünk külön állományban, de ez nem lehet forrás állomány, ugyanis a sablon önmagában nem fordítható le csak a példányosítása után. C++ nyelven ezért egy osztály-sablonnak mind az osztály definícióját, mind a metódusainak törzsét egy közös fejállományba kell helyezni, amelyet majd bemásolunk oda, ahol fel akarjuk használni. Nem kötelező, de ajánlott ennek a fejállománynak a kiterjesztését.hpp-ként írni, mert ezzel felhívjuk a figyelmet arra, hogy lényegében összevontuk azt, amit az osztályok leírásánál külön.h és.cpp állományokba szoktunk tenni, hiszen itt egy példányosítandó kódrész (sablon) található. 621

622 35. Feladat: Túlélési verseny Különféle élőlények ugyanazon a váltakozó terepekből álló pályán indulnak el sorban egymás után. Egy lénynek attól függően változik az életereje, hogy milyen terepen megy át, de közben a terepet is átalakítja. Egészen addig halad, amíg végig nem ér a pályán vagy el nem fogy az életereje és elpusztul. Egy terep akkor is átalakul, ha azon a lény elpusztul. Az első lény az eredeti pályát, a további lények az elöttük levő által átalakított pályát használják. Adjuk meg a pályán végig jutó, azaz életben maradt lények neveit! A pályán három féle terep fordulhat elő: fű, homok, mocsár. A lények három különböző fajta egyikéhez tartozhatnak. Zöldike: kezdeti életereje 10; füvön az életereje eggyel nő, homokon kettővel csökken, mocsárban eggyel csökken; a mocsaras terepet fűvé alakítja, a másik két féle terepet nem változtatja meg. Buckabogár: kezdeti életereje 15; füvön az ereje kettővel csökken, homokon hárommal nő, mocsárban néggyel csökken; a füvet homokká, a mocsarat fűvé alakítja, de a homokot nem változtatja meg. Tocsogó: kezdeti életereje 20; füvön az életerő kettővel, homokon öttel csökken, mocsárban hattal nő; a füvet mocsárrá alakítja, a másik két féle terepet nem változtatja meg. Minden lénynek van egy neve (sztring), ismert az aktuális életereje (egész szám) és a fajtája. Egy lény addig él, amíg az életereje pozitív. A verseny adatait egy szöveges állományból olvassuk be! A fájl első sora tartalmazza a lények számát, amelyet a lények soronkénti leírása követ. Ez a fajtát jelölő karakter (Z zöldike, B buckabogár, T tocsogó), amit szóköz után a lény neve követ. Ezek után következik a pálya leírása. Nemnegatív egész szám adja meg a pálya terepeinek számát (hossz), majd ezt követően a terepek leíró számok jönnek (0 homok, 1 fű, 2 mocsár). Feltehetjük, hogy a fájl formátuma helyes. 4 Z fűevő B homokfutó B pattogó 622

623 T szivacs

624 Specifikáció A feladat megoldásában központi szerepet játszanak a lények. Attól függetlenül, hogy a lények konkrétan kicsodák vagy mi a fajtájuk, számos közös tulajdonsággal rendelkeznek. Mindegyiknek van neve és életereje, meg lehet róla kérdezni, hogy hívják (Név()), él-e (Él()) még, azaz az életereje nagyobb-e nullánál, és szimulálni lehet a viselkedését a pálya egy bizonyos terepén. Ez utóbbi művelet (Átalakít()) egyrészt módosítja a lény életerejét, másrészt átalakítja a neki átadott terepet. Ennek a műveletnek a hatása attól függ, hogy egy lény milyen fajtájú, ezért ez a művelet a lények általános jellemzésének szintjén még nem implementálható. Ez nem baj, hiszen általános lényeket úgysem akarunk létrehozni. A lények leírásához bevezetünk négy osztályt. Az általános lény típusát leíró osztály absztrakt lesz. Ebből származtatjuk a konkrét fajtájú lények, zöldikék, buckabogarak és tocsogók osztályait. A származtatott osztályokban felüldefiniáljuk az Átalakít() metódust ábra. Lények osztálydiagrammja 624

625 Zöldikék esetében a kezdő életerő: 10, amit a konstruktor állít be. Az Átalakít() művelet hatását az alábbi táblázat foglalja össze. Ez megkapja bemenetként az aktuális terepet, és a táblázat megfelelő sora alapján megváltoztatja az aktuális lény életerejét és visszaadja az új terepet. terep életerő változás terepváltozás homok -2 - fű +1 - mocsár -1 fű Buckabogarak esetében a kezdő életerő: 15, és az Átalakít() művelet hatása: terep életerő változás terepváltozás homok +3 - fű -2 homok mocsár -4 fű Tocsogók esetében a kezdő életerő: 20, és az Átalakít() művelet hatása: terep életerő változás terepváltozás homok -5 - fű -2 mocsár mocsár

626 A lények absztrakt osztályát és az abból származtatott speciális lények osztályait ezek alapján már könnyen elkészíthetjük. A speciális osztályok konstruktorai meghívják az ősosztály konstruktorát, majd inicializálják az életerőt. Az Él() és Név() metódusok az ősosztály szintjén implementálhatók. Az Él() metódus akkor ad igaz értéket, ha az életerő pozitív. A Név() metódus a név adattag értékét adja vissza. Az Átalakít() metódus az ősosztályban absztrakt, a konkrét osztályok szintjén kell definiálni a korábban definiált táblázatok alapján. Ez módosítja az életerőt és megváltoztatja az adott pályamezőt. Most pedig specifikálhatjuk a teljes feladatot. A specifikációban meg kell különböztetni egy-egy lény áthaladta utáni pálya állapotokat. A nulladik változat a kezdőpálya, az i-edik az i-edik lény mozgása után kialakult pálya. A = ( kezdőpálya : N m, lények : Lény n, túlélők : String * ) Ef = ( lények = lények kezdőpálya =kezdőpálya ) Uf = ( lények = lények kezdőpálya=kezdőpálya pálya:(n m ) n pálya[0]=kezdőpálya i [1..n]: pálya[i]=eredm(lények[i],pálya[i-1]) 2 túlélők ahol az Eredm: Lény N m n i 1 Eredm( lények[ i], pálya[ i 1]) 1. Él() lények [ i]. név Lény N m függvény kiszámolja, hogy egy lénynek az adott pályán áthaladva hogyan változik az életereje és hogyan változik eközben alatta a pálya. Ezt a számítást egy rekurzív definíciójú függvénnyel lehet leírni úgy, hogy egy kezdeti életerővel rendelkező lény és egy m hosszúságú pálya esetén Eredm(lény,pálya) = r(m), ahol r:[0.. m] Lény N m r(0) = (lény,pálya) j [1..m]: r( j) Lépés ( r( j r( j 1) 1)) ha ha r( j r( j 1). Él() 1 1). Él() A Lépés(r(j-1)) állítja be a j-edik lépés utáni a lény életerejét (az r(j) 1 jelöli ekkor a lényt, amelynek a korábbi állapotát az r(j-1) 1 mutatja), valamint az 1 ) 626

627 r(j) 2 pályát (valójában a pályának csak a j-edik mezője változhat a pálya előző r(j-1) 2 állapotához képest). Ezeket a változásokat a korábban definiált Átalakít() függvény alapján számíthatjuk ki. Ez így egy kicsit bonyolultnak tűnik, de objektum orientált szemlélettel megfogalmazva a rekurzív függvény j-edik lépését az i-edik lényre már sokkal egyszerűbb felírni: lények[i].átalakít(pálya[j]), feltéve, ha lények[i].él(). Absztrakt program A megoldó programnak két szintje van. A felső szinten a fenti specifikációnak megfelelő algoritmust írjuk le, az alsó szinten a lények osztályait. A külső ciklus a lényeket veszi sorra. Minden lényt végig vezet a pályán, de csak addig, amíg él, közben átalakítja a pályát, és ha túléli a lény a pályát, akkor kiírja a nevét. i = 1..n j:=1 j m lények[i].él() lények[i].átalakít(pálya[j]) j:=j+1 lények[i].él() túlélők:=túlélők lények[i].név() SKIP 627

628 Implementálás A programkód a kommenteket nem számítva most is angol nyelvű lesz, de itt talán célszerű egy kis magyar-angol szótárt is mellékelni a megvalósításhoz. lény creature átalakít transmute zöldike greenfinch pálya field buckabogár sandbug terep ground tocsogó squelchy fű grass erő power homok sand név name mocsár swamp él alive A program komponens szerkezete Az absztrakt algoritmust a main.cpp állományban elhelyezett main függvényben találjuk. Az osztályok definíciói a creature.h fejállományba, az Transmute() metódusok implementációi a creature.cpp forrásállományba kerülnek. Főprogram kódolása A main függvény az absztrakt program kódján kívül a lények és a pálya beolvasását is tartalmazza. A versenyen résztvevő lényekre történő hivatkozásokat, azaz Creature* típusú elemeket egy tömbben (vector<creature*>) tároljuk. Ha nem a Creature osztály egy objektumát, hanem a Creature osztályból származtatott osztály egy objektumát hozzuk létre, akkor ennek hivatkozása (címe) is elhelyezhető ebben a tömbben. Így lényegében egy olyan tömböt használunk, amelyik vegyesen tárolhat különböző, de a Creature osztályból 628

629 származtatott osztályú (típusú) elemeket: a tömb egy elemének tehát alternatív szerkezetű típusa van. ifstream f("input.txt"); int n; f >> n; vector<creature*> creatures(n); for(int i=0; i<n; ++i){ char l; string a; f >> l >> a; switch(l){ case 'T' : creatures[i] = new Squelchy(a); break; case 'Z' : creatures[i] = new Greenfinch(a); break; case 'B' : creatures[i] = new Sandbug(a); break; default:; 629

630 A pálya a pályamezők számkódjait tartalmazó tömbbe (vector<int>) kerül. int m; f >> m; vector<int> field(m); for(int j=0; j<m; ++j) f >> palya[j]; A feldolgozást a struktogramm alapján kódoljuk. Figyelembe kell venni, hogy a creatures tömb a C++ megvalósításban pointereket tárol, ezért például az i-edik lény által okozott átalakítást itt a creatures[i]- >Transmute() alakú metódushívással tehetjük meg. for(int i=0; i<n; ++i){ for(int j=0; creatures[i]->alive() && j<m; ++j){ creatures[i]->transmute(palya[j]); if (creatures[i]->alive()) cout << creatures[i]->name() << endl; A program végén ne felejtsük el felszabadítani a saját memóriafoglalásainkat. for(int i=0; i<n; ++i){ delete creatures[i]; 630

631 Creature osztály Ez a specifikációnak megfelelő absztrakt osztály. Absztrakt voltára két dolog is felhívja a figyelmet: a konstruktora nem publikus és a Transmute() metódusa nincs implementálva. Ennél fogva ilyen típusú objektumot nem lehet létrehozni. class Creature { protected: std::string name; int power; Creature(std::string a):name(a) { public: std::string Name() const { return name; bool Alive() const { return power > 0; virtual void Transmute(int &gound) = 0; virtual ~Creature(){ ; Fontos, hogy az Transmute() metódus virtuális legyen, hiszen ez jelzi a fordítónak, hogy egy ilyen metódus hívását nem szabad fordítási időben kiértékelni, majd csak futás közben dől el, hogy a származtatott osztályok közül melyiknek a Transmute() metódusa fut le (dinamikus kötés). Ez pedig attól függ majd, hogy valójában milyen típusú objektumra mutat az a hivatkozás, amellyel ezt metódust meghívjuk. 631

632 Speciális lények osztályai A specifikáció meghatározta a Creature osztály leszármazott osztályait is. Ezek rendelkeznek az ősosztály védett tagjaival, csak konstruktort kell megadniuk és a Transmute() metódust felüldefiniálniuk. A konstruktorok az adott fajtájú lényre jellemző kezdeti életerőt állítják be azt követően, hogy az ősosztály konstruktorát meghívva beállítják az objektum (konkrét lény) nevét is. Ez a név a konstruktor bemenő paramétere. class Greenfinch : public Creature { public: Greenfinch(std::string a):creature(a){power=10; void Transmute(int &gound); ; class Sandbug : public Creature { public: Sandbug(std::string a):creature(a){power = 15; void Transmute(int &gound); ; class Squelchy : public Creature { public: Squelchy(std::string a):creature(a){power = 20; void Transmute(int &gound); ; 632

633 A Transmute() metódus deklarációját az utódosztályok megismétlik, majd a specifikációban megadott táblázat alapján háromféleképpen definiálják. void Greenfinch::Transmute(int &gound) { switch(gound){ case 1: power+=1; break; case 0: power-=2; break; case 2: power-=1; gound = 1; break; default:; void Sandbug::Transmute(int &gound) { switch(gound){ case 1: power-=2; gound = 0; break; case 0: power+=3; break; case 2: power-=4; gound = 1; break; default:; 633

634 void Squelchy::Transmute(int &gound) { switch(gound){ case 1: power-=2; gound = 2; break; case 0: power-=5; break; case 2: power+=6; break; default:; Ez mindhárom esetben egy elágazás, amely a bemenetként megadott tereptől függően módosít a lény életerején és megváltoztatja, ha kell, a terepet. Feltételezzük, hogy ezek a metódusok csak akkor kerülnek meghívásra, amikor a lény még él, ezért ezt itt külön nem ellenőrizzük. Tesztelés Fekete doboz tesztesetek: Érvényes adatok: 1. Nincsenek lények. 2. Nulla hosszúságú a pálya (minden lény életben marad). 3. Az első illetve az utolsó mezőre lépve fogy el egy lény életereje. 4. Egy speciális lény kipróbálása (mindhárom fajtára külön-külön) olyan pályán, ahol egymás után mindhárom talaj előfordul, és ezeken a lény végig megy (életben marad). Ehhez a teszthez érdemes kiíratni a megváltozott pályát, hogy a változásokat számszerűen is láthassuk. 5. Egy speciális lény kipróbálása (mindhárom fajtára külön-külön) olyan pályán ahol a lény életereje elfogy. 6. Általános eset sok lénnyel. 634

635 Érvénytelen adatokra nincs felkészítve a fenti program. Nem létező állomány vagy hibás formátumú állomány esetén a program elromlik. Fehér doboz tesztesetek: A fenti esetek tesztelik a program minden utasítását. Dinamikus helyfoglalások miatt viszont tesztelni kellene még a memória szivárgást. Komponens tesztre külön nincs szükség. 635

636 Teljes program main.cpp: #include <iostream> #include <fstream> #include <vector> #include "creature.h" using namespace std; int main() { ifstream f("input.txt"); int n; f >> n; vector<creature*> creatures(n); for(int i=0; i<n; ++i){ char l; string a; f >> l >> a; switch(l){ case 'T' : creatures[i] = new Squelchy(a); break; case 'Z' : creatures[i] = new Greenfinch(a); 636

637 break; case 'B' : creatures[i] = new Sandbug(a); break; default:; int m; f >> m; vector<int> palya(m); for(int j=0; j<m; ++j) f >> palya[j]; for(int i=0; i<n; ++i){ for(int j=0; creatures[i]->alive() && j<m; ++j){ creatures[i]->transmute(palya[j]); if (creatures[i]->alive()) cout << creatures[i]->name() << endl; for(int i=0; i<n; ++i) delete creatures[i]; return 0; 637

638 creature.h: #ifndef CREATURE_H #define CREATURE_H #include <string> class Creature { protected: std::string name; int power; Creature(std::string a):name(a) { public: std::string Name() const { return name; bool Alive() const { return power > 0; virtual void Transmute(int &gound) = 0; virtual ~Creature(){ ; class Greenfinch : public Creature { public: Greenfinch(std::string a):creature(a){power=10; void Transmute(int &gound); ; 638

639 class Sandbug : public Creature { public: Sandbug(std::string a):creature(a){power = 15; void Transmute(int &gound); ; class Squelchy : public Creature { public: Squelchy(std::string a):creature(a){power = 20; void Transmute(int &gound); ; #endif 639

640 creature.cpp: #include "creature.h" using namespace std; void Greenfinch::Transmute(int &gound) { switch(gound){ case 1: power+=1; break; case 0: power-=2; break; case 2: power-=1; gound = 1; break; default:; void Sandbug::Transmute(int &gound) { switch(gound){ case 1: power-=2; gound = 0; break; case 0: power+=3; break; case 2: power-=4; gound = 1; break; default:; 640

641 void Squelchy::Transmute(int &gound) { switch(gound){ case 1: power-=2; gound = 2; break; case 0: power-=5; break; case 2: power+=6; break; default:; 641

642 36. Feladat: Lengyel forma és kiértékelése Alakítsunk át egy infix formájú, egész számokból, alapműveleti jelekből és zárójelekből álló aritmetikai kifejezést postfix (lengyel) formájúra, és számoljuk ki az értékét. Specifikáció A feladat specifikációja önmagában nem ad túl sokat árul el a lehetséges megoldásról. A = ( a : String *, z : Z ) Ef = ( a = a ) Uf = ( z = érték(a ) ) A feladatot célszerű három részre felbontani: 1. Először a bemeneti adatként kapott karaktersorozatban ki kell jelölni a szintaktikai egységeket (a zárójeleket, a műveleti vagy operátor jeleket és az operandusokat). Ha például a bemenet a (11+26)*(43 4) sztring, akkor azt át kell alakítani egy token- sorozattá: <(> <11> <+> <26> <)> <*> <(> <43> < > <4> <)>, amelyben önálló elemek a szintaktikai egységek. A = ( a : String *, x : Token * ) Ef = ( a = a ) Uf = ( x = tokenizált(a ) ) 2. Az előző lépés eredményeként előállt infix formájú token-sorozatból elkészíteni annak <11> <26> <+> <43> <4> < > <*> postfix (lengyel) formájú alakját. A = ( x : Token *, y : Token * ) Ef = ( x = x infixforma(x) ) Uf = ( y = InfixbőlPostfix(x ) ) 3. A postfix formájú token-sorozatnak ki kell számolni az értékét. 642

643 A = ( y : Token *, z : Z ) Ef = ( y = y postfixforma(y) ) Uf = ( z = kiértékel(y ) ) Ezeknek a részfeladatoknak jól látszik a bemenő és kimenő adatuk, valamint az is, hogy egymás után megoldva őket az eredeti feladat megoldásához jutunk. Az egyes átalakítások során fel kell készülni arra, hogy ha az aritmetikai kifejezést kezdetben nem adták meg helyesen, akkor a feldolgozás során nem várt esetek fordulhatnak elő, amelyeket kezelni kell. Absztrakt program A megoldás központi eleme a szintaktikai egységeket, a tokeneket leíró adattípus. Ez egy alternatív szerkezetű típus, hiszen legjellemzőbb tulajdonsága az, hogy különböző fajtájú értékei lehetnek, amelyekről minden pillanatban el kell tudnunk dönteni, hogy az egy nyitó vagy csukózárójel-e, operandus-e vagy operátor. Az ilyen típus megvalósításához a származtatás eszközét használjuk fel ábra. Tokenek osztálydiagrammja 643

644 Definiáljuk a tokenek általános osztályát (absztrakt ősosztály) és ebből származtatva az egyes tokenfajták konkrét osztályait. A fajta lekérdezését biztosító Is_ kezdetű metódusok ősosztálybeli definíciójuk szerint hamis értéket adnak vissza, de a megfelelő osztályokbeli felüldefiniálásuk az igaz értéket. Így egy konkrét tokenre mindig pontosan az egyik Is_ kezdetű metódus ad csak igazat, éppen az, amilyen a token fajtája. Az operandus tokeneknek lekérdezhető az értékük. Az operátor tokeneknek megkérdezhetjük a prioritását (a szorzás és osztás magasabb prioritású az összeadásnál és kivonásnál), és kiszámolhatjuk két egész számnak az adott oparátorral elvégzett eredményét. A részfeladatok megoldásánál szükség lesz egy olyan gyűjteményre, amelyben token-sorozatot tudunk tárolni. Egy ilyen sorozatot először fel kell tölteni, mondjuk a sorozat végéhez történő hozzáfűzés műveletével, majd be kell járni az elemeit. A 34. feladatban definiáltuk a BiQueue kettős sorok osztályát, amely rendelkezik egy sorozat végére író (Hiex()t) művelettel és bejárót (Enumerator) is lehetett hozzá készíteni. Sajnos azonban az a BiQueue típusó sorozat csak egész számok tárolására alkalmas. De ha elkészítjük a BiQueue osztály olyan sablonját, amely olyan sorozatokat definiál, amelyek elemeinek típusát egy sablon-paraméter helyettesíti, akkor ezt már felhasználhatjuk akár tokenek sorozatának tárolására. Egy szintaktikailag helyes infix forámjú aritmetikai kifejezés postfix formájúra alakításának algortimusa jól ismert. x.first() y:=<> x.end() t: = x.current() t.is_operand() t.is_leftp() t.is_rightp() t.is_operator() y:hiext(t) s.push(t) s.top().is_left() s.empty() s.top().is_left() s.top().priority() 644

645 >t.priority() y:hiext(s.pop()) s.pop() y:hiext(s.pop()) s.push(t) x.next() s.empty() y:hiext(s.pop()) Az algoritmusban x és y egy-egy tokeneket tartalmazó kettős sor, az s pedig egy tokeneket tartalmazó verem: bemenő adat az x, eredmény adat az y, segéd adat az s. A verem Pop() művelete nemcsak elhagyja a verem tetején levő tokent, hanem vissza is adja azt. A postfix formájú aritmetikai kifejezés kiértékelése is egy nevzetes algoritmus. Ebben y egy tokeneket tartalmazó kettős sor, a z egy egész típusú változó, a v pedig egész számokat tartalmazó verem: bemenő adat az y, eredmény adat a z, segéd adat a v. A verem Pop() művelete nemcsak elhagyja a verem tetején számot, hanem vissza is adja azt. y.first() y.end() t = y.current() t.is_operand() v.push(t) v.push (t.evaluate(v.pop(),v.pop()) y.next() z:=v.pop() Látható, hogy mindkét fenti algoritmusnak szüksége lesz egy-egy veremre. Az első folyamatnál a verembe műveleti jelek illetve nyitó zárójelek tokenjeit kell beletenni, a második folyamatnál viszont az operandusok 645

646 értékeit, amelyek itt egész számok. Ha elkészítjük a 33. feladatban szereplő Stack osztálynak olyan egy sablonját, amellyel olyan vermek írhatók le, ahol az elemek típusát egy sablon-paraméter helyettesíti, akkor ezt felhasználhatjuk mind tokeneket tároló verem, mind egész számokat tároló verem létrehozásához. Implementálás A program komponens szerkezete A program több részből áll. A stack.hpp állományban a verem osztály-sablonját, a biqueue.hpp állományban helyezzük el a kettős sor osztály-sablonját, a token.h és token.cpp állományok tartalmazzák a tokenek ősosztályát és a tokenek fajtáinak az ősosztályból származtatott osztályait. A main.cpp állomány main függvényben találjuk a feldolgozás három lépését. Tokenek osztályai A Token osztály, valamint az abból származtatott osztályok definícióit a terv alapján készítjük el. class Token{ friend std::istream& operator>>(std::istream&, Token*&); public: class IllegalElementException{ private: char ch; public: IllegalElementException(char c) : ch(c){ 646

647 char Message() const { return ch; ; virtual ~Token(){ virtual bool Is_LeftP() virtual bool Is_RightP() const {return false; const {return false; virtual bool Is_Operand() const {return false; virtual bool Is_Operator() const {return false; virtual bool Is_End() const {return false; ; A konkrét token fajtákat kiegészítjük egy újabbal is. class End: public Token{ public: bool Is_End() const {return true; ; Kényelmesebb kezelni az elemzendő kifejezéseket, ha azok egy speciális jellel, mondjuk, pontosvesszővel vannak befejezve. A tokenizálásnál ezt a jelet speciális szintaktikai egységnek tekintjük, amely egy újabb fajta token lesz: End. Ennek egyetlen metódusa az Is_End() logikai függvény lesz, amelyet igaz értéket ad vissza. Természetesen ezt a metódust a Token ősosztályban is definiálni kell úgy, hogy ott hamis értéket adjon vissza. Így ez fog öröklődni a többi konkrét token fajta osztályára. Kiegészítjük az ős Token osztályt egy barátfüggvénnyel is, pontosabban a beolvasó operátor egy felüldefiniálásával. Ez egy nagyon 647

648 fontos eleme a megoldásunknak, ugyanis ezzel az operátorral tudjuk egy tetszőleges adatfolyam karaktersorozatából beolvasni a soron következő tokennek megfelelő karaktereket és magát a tokent létrehozni. istream& operator >> (istream& s, Token* &t) { char ch; s >> ch; switch(ch){ case '0' : case '1' : case '2' : case '3' : case '4' : case '5' : case '6' : case '7' : case '8' : case '9' : s.putback(ch); int intval; s >> intval; t = new Operand(intval); break; case '+' : case '-' : case '*' : case '/': t = new Operator(ch); break; case '(' : t = new LeftP(); case ')' : t = new RightP(); case ';' : t = new End(); break; break; break; default: if(!s.fail()) throw new Token::IllegalElementException(ch); return s; 648

649 A Token osztályban definiáljuk azt a kivétel-osztályt, amelynek példányait kivételként dobjuk a tokenizálás során, ha nem megfelelő karakterrel találkozunk. Tárolók osztály-sablonjai A tárolók (vermek és a kettős sorok) osztály-sablonjainak elkészítésekor a korábban már létrehozott Stack és BiQueue osztályokból indulunk ki (lásd előző fejezet feladatait). Ahhoz, hogy a Stack osztályból sablont készítsünk, meg kell keresnünk a definíciójában az összes olyan részletet, ahol a verembeli elemek típusára int-ként hivatkozunk. Ilyen például a Top() és a Pop() visszatérési típusa, a Push() paraméterváltozójának típusa, a beágyazott Node struktúra (amely automatikusan sablonná válik) val adattagjának és konstruktora első paraméterváltozójának típusa, valamint a Pop() lokális e változójának típusa. Ezeket mind kicsréljük az Item sablon-paraméterre. Azoknál a metódusoknál, ahol a bemenő paraméterváltozó típusaként szerepelt a lecserélendő int szó, ott ezt a const Item& típussal kell helyettesíteni, hiszen a sablon-paraméter helyébe egy példányosításnál összetett típus is kerülhet, és nem lenne szerencsés (memória pazarlás) ha a metódus bemenő paraméterének értékét lemásolva adnánk át azt a paraméterváltozójának. Természetesen el kell még helyezni a sablon jelöléséhez szükséges nyelvi elemeket: az osztály és az azon kívül definiált metódusai elé (template <typename Item>), a kívül definiált metódusok neve előtt a Stack<Item>:: minősítést használjuk, továbbá a másoló konstruktor visszatérési típusát is Stack<Item>-re kell cserélni Hasonló tennivalónk van a BiQueue sablonosításánál. Ne feledkezzünk meg a beágyazott Enumerator osztály Current() metódusának visszatérési típusáról sem. A beágyazott osztály ugyanis ugyanúgy sablonná válik, mint az beágyazó. Ügyeljünk arra, hogy ne automatikus cserét végezzünk, mert vannak a kódban olyan int definíciók, amelyeket nem 649

650 szabad Item-re cserélni (ilyen például az enumeratorcount adattag típusa). meg. Főprogram Mindkét sablont a feladat végén elhelyezett teljes kódban tekinthetjük A main függvény a tervnek megfelelően három szakaszból áll. Az első szakasz a tokenizálást végzi. Ez a szabványos bemenetről beolvasott pontosvesszővel lezárt sztringet bontja fel tokenekre. Itt használjuk a Token osztálynál definiált beolvasó operátort, amely a soron következő tokent találja meg: létrehozza azt és visszaadja a címét. Ezeket a címeket a Token* típussal példányosított BiQueue típusú x kettős sorban helyezzük el. Ez a folyamat addig tart, amíg nem olvassuk be a vége tokent. A beolvasás kivételt dob, ha nem értelmezhető karaktert talál. Ezeket a kivételeket elkapjuk, ezután hibaüzenettel leállítjuk a programot, de még ez előtt töröljük a kettős sorben tárolt címeken található tokeneket a dinamikus memóriából (DeallocateToken()), és töröljük magát a kivétel objektumot is. BiQueue<Token*> x; try{ Token *t; cin >> t; while(!t->is_end()){ x.hiext(t); cin >> t; catch(token::illegalelementexception *ex){ cout << "Illegális karakter: " 650

651 << ex->message() << endl; delete ex; DeallocateToken(x); exit(1); A második szakasz a tervben megadott algoritmust kódolja. Az x kettős sor elemeinek bejárásához egy felsorló objektumot hozunk létre. Token* típussal példányosítjuk a Stack-et és a BiQueue-t, így definiáljuk a megoldáshoz szükséges s vermet és az eredményt tartalmazó y kettős sort. A kódba minden olyan ponton, hibaellenőrzést építünk be, amelyre szintaktikusan hibás aritmetikai kifejezés esetén kerülne a vezérlés: ilyen a verem idő előtti kiürülése vagy nem várt token előfordulása. Jól megfigyelhető a kódban a polimorfizmus jelensége, nevezetesen az, amikor a Token* típusú t változóra meghívjuk például az Is_Operator()-t vagy valamelyik másik virtuális lekérdező metódust. Ezek a hívó utasítások nem értelmezhetőek fordítási időben, mert a hatásuk attól függ, hogy futási időben éppen milyen fajta tokenre mutató cím található a t-ben. A t->is_operator() eredménye attól függően lesz igaz vagy hamis, hogy a t egy operátorfajta tokenre mutat vagy sem. Egészen más a helyzet ((Operator*)s.Top())->Priority() hivatkozással. Az s.top()->priority() kifejezést a fordító önmagában nem tudja értelmezni, hiszen a Token osztálynak nincs Priority() metódusa. Ha viszont az s.top() értékét Operator* típusú címmé konvertáljuk (az öröklődési kapcsolat miatt ezt szabad), akkor mivel az Operator-nak van Priority() metódusa a kifejezés már lefordítható. Ez az úgynevezett statikus konverzió (static_cast<operator*>(s.top())), amelyet a kódban az egyszerűbb C nyelvi írásmóddal (((Operator*)s.Top())) jelölünk. Ez a megoldás veszélyes lehet, ha a t változóba futás közben más fajtájú token címe is kerülhetni, mont operátor. Itt azonban biztosak lehetünk abban, hogy amikor ez a kifejezés kiértékelésre kerül, akkor az s verem tetején operátor fajtájú 651

652 token van (lásd a kifejezést beágyazó ciklus feltételét). Ha ilyen bizonyosságunk nem lenne és csak futási időben derülhetne ki, hogy egy ilyen átalakítás helyes-e vagy sem, akkor az úgynevezett dinamikus konverziót kellene alkalmazni ((dynamic_cast<operator*>(s.top()))). A megoldás harmadik szakaszának kódja a tervben megadott második algoritmust követi, de ebben is elhelyezünk néhány hibaellenőrzést. Végül megjegyezzük, hogy a kódban sehol sem figyelünk a vermek esetleges FULLSTACK kivételére. Tesztelés A BiQueue és a Stack tesztelését már korábban megtettük. A feladat fekete doboz tesztelésének keretében az érvényes tesztadatok a szintaktikusan helyes aritmetikai kifejezések lesznek. Ezek között feltételnül meg kell vizsgálni a többszörösen zárójelezett kifejezéseket, olyanokat, ahol különböző priorítású műveleti jelek különféle sorrendben fordulnak elő egy zárójelezetlen részben. Érvénytelen adatok a különféle szintaktikusan helytelen kifejezések. A tesztesetek részletes kidolgozását az Olvasóra bízzuk. 652

653 Teljes program main.cpp: #include <iostream> #include <cstdlib> #include token.h #include stack.hpp #include biqueue.hpp using namespace std; void DeallocateToken(BiQueue<Token*> &x); int main() { cout << Add meg az aritmetikai kifejezést!\n ; cout << Írj a végére pontosvesszőt!\n ; // Tokenizálás BiQueue<Token*> x; try{ Token *t; 653

654 cin >> t; while(!t->is_end()){ x.hiext(t); cin >> t; catch(token::illegalelementexception *ex){ cout << Illegális karakter: << ex->message() << endl; delete ex; DeallocateToken(x); exit(1); 654

655 // Lengyel formára hozás BiQueue<Token*> y; Stack<Token*> s; BiQueue<Token*>::Enumerator itx = x.createenumerator(); for(itx.first();!itx.end(); itx.next()){ Token *t = itx.current(); if(t->is_operand()) else if (t->is_leftp()) y.hiext(t); s.push(t); else if (t->is_rightp()){ try{ while(!s.top()->is_leftp()) y.hiext(s.pop()); s.pop(); catch(stack<token*>::exceptions ex){ if(stack<token*>::emptystack == ex){ cout << Szintaktikai hiba! << endl; DeallocateToken(x); exit(1); 655

656 else if (t->is_operator()) { while(!s.empty() && s.top()->is_operator() && ((Operator*)s.Top())->Priority() > ((Operator*)t)->Priority() ) y.hiext(s.pop()); s.push(t); else{ cout << Szintaktikai hiba! << endl; DeallocateToken(x); exit(1); while(!s.empty()){ if(s.top()->is_leftp()){ cout << Szintaktikai hiba! << endl; DeallocateToken(x); exit(1); else y.hiext(s.pop()); 656

657 // Kiértékelés try{ Stack<int> v; BiQueue<Token*>::Enumerator ity = y.createenumerator(); for(ity.first();!ity.end(); ity.next()){ Token *t = ity.current(); if (t->is_operand()) v.push( ((Operand*)t)->Value() ); else v.push( ((Operator*)t)-> Evaluate(v.Pop(),v.Pop()) ); int r = v.pop(); if(!v.empty()) { cout << Szintaktikai hiba! << endl; DeallocateToken(x); exit(1); cout << A kifejezes erteke: << r << endl; catch(stack<int>::exceptions ex){ if(stack<int>::emptystack == ex){ 657

658 cout << Szintaktikai hiba! << endl; DeallocateToken(x); exit(1); DeallocateToken(x); return 0; void DeallocateToken(BiQueue<Token*> &x) { BiQueue<Token*>::Enumerator itx = x.createenumerator(); for(itx.first();!itx.end(); itx.next()){ delete itx.current(); 658

659 token.h: #ifndef TOKEN_H #define TOKEN_H #include <string> #include <sstream> class Token{ friend std::istream& operator>>(std::istream&, Token*&); public: class IllegalElementException{ private: char ch; public: IllegalElementException(char c) : ch { char Message() const { return ch; ; virtual ~Token(){ virtual bool Is_LeftP() virtual bool Is_RightP() const {return false; const {return false; virtual bool Is_Operand() const {return false; 659

660 virtual bool Is_Operator() const {return false; virtual bool Is_End() const {return false; ; class Operand: public Token{ public: Operand(int v) {val=v; bool Is_Operand() const {return true; int Value() const {return val; protected: int val; ; 660

661 class Operator: public Token{ public: Operator(char o) {op = o;; bool Is_Operator() const {return true; int Priority() const; int Evaluate(int a, int b) const; protected: char op; ; class RightP: public Token{ public: bool Is_RightP() const {return true; ; class LeftP: public Token{ public: bool Is_LeftP() const {return true; ; class End: public Token{ public: bool Is_End() const {return true; ; 661

662 662 #endif

663 token.cpp: #include token.h #include <sstream> #include stack.hpp using namespace std; istream& operator >> (istream& s, Token* &t) { char ch; s >> ch; switch(ch){ case 0 : case 1 : case 2 : case 3 : case 4 : case 5 : case 6 : case 7 : case 8 : case 9 : s.putback(ch); int intval; s >> intval; t = new Operand(intval); break; case + : case - : case * : case / : t = new Operator(ch); break; case ( : t = new LeftP(); case ) : t = new RightP(); case ; : t = new End(); break; break; break; default: if(!s.fail()) throw new 663

664 Token::IllegalElementException(ch); return s; int Operator::Priority() const { switch(op){ case + : case - : return 1; case * : case / : return 2; default: return 3; int Operator::Evaluate(int a, int b) const { switch(op){ case + : return a+b; case - : return a-b; case * : return a*b; case / : return a/b; default:; return 0; stack.hpp: 664

665 #ifndef STACK_HPP #define STACK_HPP #include <iostream> #include <memory> template <typename Item> class Stack{ public: enum Exceptions{EMPTYSTACK, FULLSTACK; Stack(); ~Stack(); Stack(const Stack&); Stack& operator=(const Stack&); void Push(const Item &e); Item Pop(); Item Top() const; bool Empty() const; private: struct Node{ Item val; Node *next; 665

666 Node(const Item &e, Node *n) : val, next(n){ ; Node *head; ; template <typename Item> Stack<Item>::Stack(): head(null){ template <typename Item> Stack<Item>::~Stack() { Node *p; while(head!= NULL){ p = head; head = head->next; delete p; template <typename Item> void Stack<Item>::Push(const Item &e) { 666

667 try{ head = new Node(e,head); catch(std::bad_alloc o){ throw FULLSTACK; template <typename Item> Item Stack<Item>::Pop() { if(null == head) throw EMPTYSTACK; Item e = head->val; Node *p = head; head = head->next; delete p; return e; template <typename Item> Item Stack<Item>::Top()const { if(null == head) throw EMPTYSTACK; return head->val; template <typename Item> bool Stack<Item>::Empty()const { return NULL == head; 667

668 template <typename Item> Stack<Item>::Stack(const Stack& s) { if(null == s.head) head = NULL; else { try{ head = new Node(s.head->val,NULL); catch(std::bad_alloc o){ throw FULLSTACK; Node *q = head; Node *p = s.head->next; while(p!= NULL){ try{ q->next = new Node(p->val,NULL); catch(std::bad_alloc o){throw FULLSTACK; q = q->next; p = p->next; template <typename Item> Stack<Item>& Stack<Item>::operator=(const Stack& s) { 668

669 if(&s == this) return *this; Node *p; while(head!= NULL){ p = head; head = head->next; delete p; if(null == s.head) head = NULL; else { try{ head = new Node(s.head->val,NULL); catch(std::bad_alloc o){throw FULLSTACK; Node *q = head; Node *p = s.head->next; while(p!= NULL){ try{ q->next = new Node(p->val,NULL); catch(std::bad_alloc o){throw FULLSTACK; q = q->next; p = p->next; return *this; 669

670 #endif 670

671 biqueue.hpp: #ifndef BIQUEUE_HPP #define BIQUEUE_HPP #include <memory> template <typename Item> class BiQueue{ public: enum Exceptions{EMPTYSEQ, UNDERTRAVERSAL; BiQueue(): first(null),last(null),enumeratorcount (0){ BiQueue(const BiQueue&); BiQueue& operator=(const BiQueue&); ~BiQueue(); void Loext(const Item &e); Item Lopop(); void Hiext(const Item &e); Item Hipop(); private: struct Node{ Item val; Node *next; Node *prev; Node(const Item &c, Node *n, Node *p) 671

672 : val, next(n), prev(p){; ; Node *first; Node *last; int enumeratorcount; public: class Enumerator{ public: Enumerator(BiQueue *p):bq(p),current(null) {++(bq->enumeratorcount); ~Enumerator(){--(bq->enumeratorCount); Item Current()const {return current->val; void First() {current = bq->first; bool End() const {return NULL == current; void Next() {current = current->next; private: BiQueue *bq; Node *current; ; Enumerator CreateEnumerator() {return Enumerator(this); ; template <typename Item> BiQueue<Item>::~BiQueue(){ 672

673 Node *p, *q; q = first; while( q!= NULL){ p = q; q = q->next; delete p; template <typename Item> BiQueue<Item>::BiQueue(const BiQueue &s){ if(null == s.first)first = last = NULL; else{ Node *q = new Node(s.first->val,NULL,NULL); first = q; for(node *p=s.first->next;p!=null;p=p->next){ q = new Node(p->val,NULL,q); q->prev->next = q; last = q; template <typename Item> 673

674 BiQueue<Item>& BiQueue<Item>::operator=( const BiQueue &s){ if(&s == this) return *this; Node *p = first; while(p!= NULL){ Node *q = p->next; delete p; p = q; if(null == s.first) first = last = NULL; else{ Node *q = new Node(s.first->val,NULL,NULL); first = q; for(node *p=s.first->next;p!=null;p=p->next){ q = new Node(p->val,NULL,q); q->prev->next = q; last = q; return *this; template <typename Item> void BiQueue<Item>::Loext(const Item &e){ 674

675 Node *p = new Node(e,first,NULL); if(first!= NULL) first->prev = p; first = p; if(null == last) last = p; template <typename Item> Item BiQueue<Item>::Lopop(){ if(enumeratorcount!= 0) throw UNDERTRAVERSAL; if(null == first) throw EMPTYSEQ; int e = first->val; Node *p = first; first = first->next; delete p; if(first!= NULL) first->prev = NULL; else last = NULL; return e; template <typename Item> void BiQueue<Item>::Hiext(const Item &e){ Node *p = new Node(e,NULL,last); if(last!= NULL) last->next = p; last = p; 675

676 if(null == first) first = p; template <typename Item> Item BiQueue<Item>::Hipop(){ if(enumeratorcount!= 0) throw UNDERTRAVERSAL; if(null == last)throw EMPTYSEQ; int e = last->val; Node *p = last; last = last->prev; delete p; if(last!= NULL) last->next = NULL; else first = NULL; return e; #endif 676

677 37. Feladat: Bináris fa bejárása Készítsünk egy bináris fa-típust! A típusnak támogatnia kell a fa pre-, in- és postorder bejárását! Egy bejárásnál paraméterként lehessen megadni azt a tevékenységet, amit a bejáráskor az egyes csúcsokon kell majd végrehajtani! Definiáljunk ilyen tevékenységeket a fa csúcsaiban tárolt értékek kiírására, a csúcsbeli értékek összegzésére és a belső csúcsbeli értékek maximumának meghatározására! Specifikáció A bináris fa típusának jellemző műveletei: Preorder bejárás Inorder bejárás Postorder bejárás Új érték új csúcsként való beillesztése Eldönteni, hogy egy csúcs levélcsúcs-e Eldönteni, hogy egy csúcs belső csúcs-e A három bejárás mindegyikének paraméterként egy úgynevezett tevékenység objektumot lehet majd átadni. A tevékenység objektum rendelkezik egy hajts vége művelettel, amelyik bemenő adatként a bejáráskor érintett csúcsot kapja meg. A bejárás végig adogatja a tevékenység objektumot a bináris fa csúcsain, és minden csúcsra rendre meghívja a tevékenység objektum hajts vége műveletét. Bizonyos tevékenységek valamilyen eredményt számolnak a bejárt csúcsok értékeiből (összeg, maximális érték stb.), ezért egy tevékenység objektum rendelkezhet privát adattagokkal, amelyeket az objektum létrehozása inicializál, a hajts vége művelete módosít, és ezek természetesen lekérdezhetőek. A fába új elemet beszúr műveletet ismételt meghívásával leszünk majd képesek egy bináris fát felépíteni. Ezt a műveletet most úgy valósítjuk 677

678 majd meg, hogy az a fának egy véletlenszerűen kiválasztott ágának végére függessze fel az új csúcsot. Absztrakt program A feladat megoldásához több egymáshoz szorosan kapcsolódó osztálysablont hozunk létre. Egy bináris fát láncoltan ábrázolunk, amelynek alapja a láncolt csúcs. Ez a fa egy csúcsát reprezentáló olyan listaelem, amelynek két mutatója van: egyik a csúcs baloldali gyerekét ábrázoló listaelemre, a másik a jobboldali gyereket ábrázoló listaelemre mutat. A láncolt csúcs tartalmazza a fa csúcsában tárolt értéket is. Egy láncolt csúcsot a LinkedNode<Item> osztály-sablonból lehet példányosítani. A sablon-paramétere a csúcsban tárolt érték típusa. Egy láncolt csúcsnak lekérdezhető az értéke, valamint az, hogy belső csúcsa-e az őt tartalmazó fának, vagy levélcsúcsa. A bináris fának a típusát ugyancsak osztály-sablon (BinTree<Item>) írja le, hiszen a csúcsokban tárolt elemi értékek típusát is sablon-paraméterrel jelöljük. Ezt a típust a bináris fa példányosításánál kell majd megadni. Egy fát a gyökerét adó láncolt csúcsra mutató root pointer reprezentálja, amely üres fa esetén nil értékű. Ez a fa osztály-sablonjának egy privát adattagja lesz. Négy metódussal látjuk el a fát: egy értéket új csúcsként véletlenszerűen beszúró RandomInsert() műveletettel (paramétere a beszúrandó új érték), és a három féle bejárást biztosító PreOrder(), InOrder() és PostOrder() metódusokkal (paraméterük a bejárás során az egyes csúcsokra végrehajtott tevékenység lesz). Ez utóbbiak rendre a Pre(), In() és Post() privát metódusokat hívják meg a fa gyökerére és megadott tevékenységgel. A Pre(), In() és Post() olyan rekurzív alprogramok (a bejárásoknak a klasszikus megvalósítása ugyanis rekurzív programmal történik), amelyek a paraméterként megkapott csúcs alatti részfát járják be a megfelelő stratégiával. A terveben sem a bejárások programjait, sem az új elem beszúrását végző algoritmust nem részletezzük, ezek a szakirodalomból ismertek. 678

679 Item Item Item Item ábra. Bináris fa osztálydiagrammja A bejárások egy tevékenység objektumot kapnak paraméterként. Ez rendelkezik egy Exec() metódussal, amelyiknek oda kell adni a bejárás során érintett aktuális csúcsot, mert a tevékenység ezzel, pontosabban ennek értékével hajt végre valamilyen akciót. Mivel többféle tevékenység képzelhető el és a fa bejárását végző metódusokat általánosan, tetszőleges tevékenység esetére kell definiálni, ezért el kell készítenünk a tevékenységek ősosztályát. Az Action osztály absztrakt virtuális Exec() metódusát kell a megfelelő módon felüldefiniálni a konkrét tevékenységeket leíró utódosztályokban. Elkészítjük a láncolt csúcs ősosztályát is, az absztrakt csúcs típusát. Ennél fogva kétféle csúcs fogalmat vezetnünk be: az absztrakt csúcs és a láncolt csúcs fogalmát. Az utóbbi rendelkezik azokkal a memória címekkel is, ahol a csúcs bal- és jobboldali gyerekét megtaláljuk, az előbbi nélkülözi 679

680 ezeket, csak egy értéke van, de eldönthető óla, hogy levélcsúcs-e vagy belső csúcs. Annak eldöntése, hogy egy csúcs levélcsúcs-e éppen az ellenkező eredményt adja, mint amikor azt vizsgáljuk, hogy belső csúcs-e. Ezt az inverz kapcsolatot a metódusok implementálásánál érdemes kihasználni. Az absztrakt csúcs típusa is osztály-sablon (Node<Item>), hiszen egy csúcs értékének típusát csak később szeretnénk megadni. Természetesen absztrakt csúcsot nem lehet majd létrehozni, csak a láncolt csúcs osztályának (LinkedNode<Item>) őséül szolgál, de lehetővé teszi például a tevékenység objektumok Exec() műveleténél egy absztrakt csúcsra történő hivatkozást, mert ott úgyis csak a csúcs értéke érdekel bennünket. A modellünk akkor lesz konzisztens, ha az Action is osztály-sablon, ennek is sablon-paramétere a bináris fa csúcsaiban tárolt értékek típusa. A konkrét tevékenységek definiálásához több féle Action<Item> osztály-sablonból származtatott tevékenység osztályt kell bevezetnünk. Szükség lesz a teszteléshez egy kiíró (Printer) tevékenységre. Ez is osztály-sablon, hiszen bármilyen típusú értékek kiírására képes, ha típusra definiálták a kiíró operátort. Konkrét alkalmazásához ezért majd példányosítani kell. Konstruktorának paraméterként adjuk meg azt a kimeneti adatfolyamot, ahová írni szeretnénk. Az adatfolyamra történő hivatkozást privát adattagként felvesszük az osztály-sablonba. Egy csúcs értékének kiírását az Exec() metódus végzi. Csak egész számokat tartalmazó bináris fára fogalmazzuk meg az összegzés és a feltételes maximumkeresés tevékenységeket. Ezek tehát az Action egész számokra példányosított változatából származtatott osztályok lesznek. Az összegzést (Summation) a bináris fa csúcsaiban tárolt egész számokra, a maximumkeresést (MaxSearch) a belső csúcsokban található egész számokra (ez most a feltétel) fogalmazzuk meg. Az összegzés esetében arra kell emlékeznünk, hogy az összegzés programozási tételében szereplő ciklus előtt szerepel egy s:=0 inicializáló lépés, a ciklusmagban pedig egy s:=s+aktuális érték értékadás, ahol az aktuális értéket valamilyen felsoroló szolgáltatja. Most az s változót privát adattagként vesszük fel a Summation osztályba, annak konstruktora végzi el az s:=0 értékadást, és az Exec() metódusába kerül az s:=s+aktuális csúcs 680

681 értéke értékadás. Az s változó értékét a bejárás végeztével a Result() metódussal kérdezhetjük le. A feltételes maximumkeresés esetében három privát adattagot veszünk fel a MaxSearch osztályba: l logikai érték jelzi, hogy találtunk-e belső csúcsot, a max változó annak a belső csúcsnak az értéke, amelyik a belső csúcsok között a legnagyobb értékkel bír. A konstruktor az l:=hamis értékadásból áll (ez a feltételes maximumkeresés tételében a ciklus előtti értékadás). Az Exec() metódus a programozási tétel ciklusmagja: ha az aktuális csúcs belső csúcs és korábban még nem találtunk belső csúcsot, akkor l legyen igaz és a max vegye fel az aktuális csúcs értékét; ha l már igaz volt, akkor hasonlítsuk össze a max-ot az aktuális csúcs értékével, és a kettő közül a nagyobb legyen a max új értéke. Az l és a max értékét a bejárás után a Found() és a MaxValue() metódusokkal kérdezhetjük le. Item Item Item ábra. Tevékenységek osztálydiagrammja Implementálás A program komponens szerkezete 681

682 A program két részből áll. A bintree.hpp állományban helyezzük el az Action, Node, LinkedNode, BinTree osztály-sablonokat, a main.cpp állomány tartalmazza a konkrét tevékenység osztályokat (Pinter, Summation, Maxsearch) és main függvénybe ágyazott tesztprogramot. 682

683 Action osztály template < typename Item> class Action{ public: ; Node osztály virtual void Exec(Node<Item> *node)=0; A Node osztály-sablonban a Value() egy csúcs értékét kérdezi le, az IsLeaf() eldönti, hogy a csúcs levélcsúcs-e, az IsInternal() pedig, hogy belső csúcs-e. Látható, hogy az IsInternal() törzse az IsLeaf() seítségével lett definiálva, de az IsLeaf() metódus absztrakt. template < typename Item> class Node { public: Item Value() const {return val; virtual bool IsLeaf() const = 0; bool IsInternal() const {return!isleaf(); protected: Node(const Item &v): val(v){ Item val; ; 683

684 LinkedNoded osztály A Node osztály-sablonból származtatjuk a LinkedNode osztály-sablont. Kiegészítjük a bal illetve jobboldali gyerekére mutató pointertagokkal, felüldefiniáljuk az IsLeaf() metódust, hiszen most már a gyerekekre mutató pointerek értéke alapján ez a tulajdonság kiszámolható, és ezzel implicit módon az IsInternal()-t is definiáljuk. Mivel meg akarjuk engedni, hogy a BinTree osztály-sablon lássa a LinkedNode osztály-sablon privát tagjait, ezért a BinTree osztály-sablont barátként kell megjelölni. Tekintettel azonban arra, hogy a fordító itt még nem tudhatja mi az a BinTree, ezért a LinkedNode osztály-sablon előtt deklarálni kell azt. template < typename Item> class BinTree; template < typename Item> class LinkedNode: public Node<Item>{ friend class BinTree; public: LinkedNode(const Item& v, LinkedNode *l, LinkedNode *r) :Node<Item>(v), left(l), right { bool IsLeaf() const {return NULL == left && NULL == right; private: LinkedNode *left; LinkedNode *right; ; 684

685 Bintree osztály A bináris fa osztály-sablonjának publikus része elsőként egy üres fát létrehozó konstruktort definiál. Egy fát megszüntető destruktort majd később implementáljuk. A RandomInsert metódus segítségével véletlenszerűen építünk fel egy bináris fát úgy, hogy megadva neki egy értéket, ahhoz egy olyan új csúcsot generálunk a fában, amely ezt az értéket tartalmazni fogja. Ehhez kapcsolódik a konstruktorban a véletlenszám generátor srand(time(null))-lal (#include <cstdlib>, #include <time.h>) történő életrehívása. A bináris fa osztály-sablonjában definiáljuk a három nevezetes fa-bejárási stratégiát: a PreOrder(), InOrder() és PostOrder() metódusokat. Ezek paramétere egy tevékenység objektum címe, amelyet a gyökérelemtől indulva vezetnek végig a fa csúcsain. A bináris fa osztály-sablonjának rejtett része tartalmazza a fa gyökerére mutató root pointert, valamint a különböző stratégiájú bejárásoknál meghívható Pre(), In() és Post() metódusokat. Ezeknek egyik bemenő paramétere annak a csúcsnak a pointere, amely annak a részfának a gyökerét jelzi, amelyre el akarjuk indítani a bejárást; a másik annak a tevékenységnek a pointere, amit az egyes csúcsoknál végre kell hajtani. Védettként deklaráljuk a másoló konstruktort és az értékadás operátort, hogy letiltsuk a használatukat. template < typename Item> class BinTree{ public: BinTree():root(NULL){srand(time(NULL)); virtual ~BinTree(); void RandomInsert(const Item& e); void PreOrder (Action<Item> *todo) 685

686 {Pre (root, todo); void InOrder (Action<Item> *todo) {In (root, todo); void PostOrder(Action<Item> *todo) {Post(root, todo); protected: LinkedNode<Item> *root; void Pre(LinkedNode<Item> *r,action<item>*todo); void In(LinkedNode<Item> *r, Action<Item>*todo); void Post(LinkedNode<Item> *r,action<item>*todo); ; BinTree(const BinTree&); BinTree& operator=(const BinTree&); A bináris fa bejáró műveletei a bejárásokat rekurzív módon írják le. Ezen belül a todo által mutatott tevékenység Exec() metódusát kell az aktuális csúcsra meghívni. Itt is tanúi lehetünk a dinamikus kötés jelenségének. A todo->exec() hívást ugyanis nem lehet fordítási időben meghatározni (erre figyelmeztet az Exec() metódus virtuális volta), hiszen nem tudhatjuk, hogy konkrétan milyen típusú tevékenység objektumra mutat a todo. Az alábbi metódusokban jól látható a három féle fabejárást végző rekurzív algoritmus. Üres részfa esetén egyik sem kezdeményez rekurzív hívást, nem üres részfa esetén a megfelelő sorrendben történik a részfa gyökerének todo->exec() általi feldolgozása és a bal illetve jobboldali 686

687 részfára történő rekurzív hívás. Mivel egyre kisebb részfákra hívjuk meg az alprogramot, véges lépésen belül üres részfákhoz fogunk jutni, azaz nem fordulhat elő a rekurzív hívásoknak végtelen hosszú láncolata. template < typename Item> void BinTree<Item>:: Pre(LinkedNode<Item> *r,action<item> *todo) { if(null == r) return; todo->exec(r); Pre(r->left, todo); Pre(r->right, todo); template < typename Item> void BinTree<Item>:: In(LinkedNode<Item> *r, Action<Item> *todo) { if(null == r) return; In(r->left, todo); todo->exec(r); In(r->right, todo); template < typename Item> 687

688 void BinTree<Item>:: Post(LinkedNode<Item> *r,action<item> *todo) { if(null == r) return; Post(r->left, todo); Post(r->right, todo); todo->exec(r); Érdekes és egyben hasznos alkalmazása a tevékenység objektumoknak a bináris fa egy csúcsát megszüntető tevékenység létrehozása. Ennek típusát az alábbi osztály írja le. template < typename Item> class DelAction: public Action<Item>{ public: void Exec(Node<Item> *node){delete node; ; Ha példányosítunk egy ilyen tevékenység objektumot és végig vezetjük őt a bináris fán a postorder bejárással, akkor ezzel felszabadítjuk a fa összes csúcsát, azaz megszüntetjük a fát. A bináris fa destruktorának éppen erre van szüksége. (Vigyázat! A másik két bejárás erre nem alkalmas.) Ha a DelAction osztály definícióját a bináris fa osztály-sablonjának rejtett részébe ágyazzuk, akkor egyrészt nem kell előtte feltüntetni a template <class Item> sort, másrészt elég a destruktorban DelAction-t írni a DelAction<Item> del helyett. 688

689 template < typename Item> BinTree<Item>::~BinTree() { DelAction del; ost(root, &del); Végül megadjuk a bináris fába új csúcsot véletlenszerűen beillesztő metódus implementációját. Ez a metódus egy ciklusban generál véletlenszerű jobb illetve bal értékeket amíg nem talál olyan csúcsot, amelyiknek a legutoljára generált oldalán nincs csúcs. Ide függeszt fel a beszúrandó értéket tartalmazó új csúcsot az alábbi kód. template < typename Item> void BinTree<Item>::RandomInsert(const Item& e) { if(null == root) root = new LinkedNode<Item>(e,NULL,NULL); else { LinkedNode<Item> *r = root; int d = rand(); while(d&1? r->left!=null : r->right!=null){ if(d&1) r = r->left; else r = r->right; d = rand(); 689

690 if(d&1) r->left = new LinkedNode<Item>(e,NULL,NULL); else r->right = new LinkedNode<Item>(e,NULL,NULL); A kódban néhány olyan érdekes C++ nyelvi elem került, mint a feltételes kifejezés (feltétel? kifejezés1 : kifejezés2), vagy a bitenkénti és művelet, amellyel leválasztjuk a véletlen szám legutolsó bitjét, hogy azt jobb illetve bal értéknek tekintsük. Tevékenység osztályok Egy tevékenység osztályt már definiáltunk, ez volt a DelAction. Adjuk meg most a többit is. A Maxsearch típusú tevékenység megkeresi a belső csúcsokban a legnagyobb értéket. A konstruktorban a keresés sikerességét jelző logikai értéket inicializáljuk, ennek, valamint a max tagnak az értékét módosítja az Exec(), az eredményt pedig a Found() és a MaxValue() segítségével kérdezhetjük le. class Maxsearch: public Action<int>{ public: Maxsearch(){l = false; void Exec(Node<int> *node){ if(node->isleaf()){ if(!l){ 690

691 l = true; max = node->value(); else if(node->value()>max) max = node->value(); bool Found(){return l; int MaxValue(){return max; private: int max; bool l; ; A Summation típusú tevékenység hozzáadja az aktuális csúcs értékét konstruktorban nullának inicializált s adattaghoz. A Result() ennek aktuális értéket adja vissza. 691

692 class Summation: public Action<int>{ public: Summation(): s(0){ void Exec(Node<int> *node){s+=node->value(); int Result(){return s; private: int s; ; A Printer osztály-sablon tevékenység objektumai egy csúcs értékét írják ki. A tevékenység paramétere a konstruktorában beállítható kimeneti folyam. Az osztály-sablonból egy konkrét kiíró tevékenység a Printer<int> print(cout) utasítással hozható belőle létre. template < typename Item> class Printer: public Action<Item>{ public: Printer(ostream &o): s(o){; void Exec(Node<Item> *node) {s << [ << node->value() << ] ; private: ostream& s; ; Főprogram 692

693 Az alábbi kód a szabványos bemenetről beolvasott értékekkel véletlenszerűen épít fel egy bináris fát. BinTree<int> t; int i; while(cin >> i) { t.randominsert(i); Ennek tartalmát különféle bejárási stratégiák mellett írjuk ki a standard kimenetre, majd meghatározzuk a csúcsokban tárolt értékek összegét és a belső csúcsok értékeinek maximumát. Printer<int> print(cout); cout << Preorder bejárás: ; t.preorder(&print); cout << endl; cout << Inorder bejárás: ; t.inorder(&print); cout << endl; cout << Postorder bejárás: ; t.postorder(&print); cout << endl; 693

694 Summation sum; t.preorder(&sum); cout << Fa elemeinek összege: << sum.result() << endl; Maxsearch ms; t.preorder(&ms); cout << Maximum of internal elements:\n ; if(ms.found()) cout << ms.maxvalue() << endl; else cout << none << endl; 694

695 Tesztelés Egy olyan programot nehéz tesztelni, amelyik véletlenszerűen állítja elő a bemenő adatokat. Szerencsére most nem egészen erről van szó, hiszen a bináris fába betett értékeket mi adhatjuk meg, csak azok fába beillesztése véletlenszerű. Ez nem akadályozza meg a fa kiírásának tesztelését, az összegzés tesztelését, egyedül a maximumkeresés esetében nehéz az olyan tesztadatok előállítása, amikor a legelső, vagy a legutolsó adatot szeretnék maximálisnak választani. Ha másképpen nem megy, külön tesztprogramot kell készíteni. Külön komponens tesztet nem készítünk, az alábbi tesztesetek kielégítőek. Fekete doboz tesztesetek: Érvényes adatok: 1. Üres fa esete. 2. Egyetlen csúcs (gyökércsúcs, amely ilyenkor levél csúcs is) esete. 3. Két csúcs beillesztése. (Egy belső csúcs és egy levélcsúcs lesz) 4. Több csúcs, legalább két belső csúcs beillesztése. Ehhez próbálgatással juthatunk el. (Itt érdemes az összegzést illetve a feltételes maximumkeresést nemcsak a preorder, hanem a másik kettő stratégiával is kipróbálni.) 5. Általános eset. Érvénytelen adatok nem lehetnek, viszont a memória elfogyást ki lehet mérni, de erre nem alkalmas a jelenlegi főprogram, hiszen az interaktív. Fehér doboz tesztesetek: 1. Erre még inkább igaz az, amit a véletlen feltöltésről az előbb mondtunk. Nehéz például letesztelni a RandomInsert() metódus minden utasítását. Nagyszámú adat megadása esetén azonban legalább 50%-os valószínűséggel minden utasítására rákerül a vezérlés. Ugyanez mondható el a bejárások utasításairól is. 2. Dinamikus helyfoglalások miatt vizsgálni kellene még a memóriaszivárgást. 695

696 Teljes program main.cpp: #include "bintree.hpp" #include <iostream> using namespace std; template < typename Item> class Printer: public Action<Item>{ ostream& s; public: Printer(ostream &o): s(o){ void Exec(Node<Item> *node) {s << '['<< node->value() << ']'; ; class Summation: public Action<int>{ public: Summation(): s(0){ void Exec(Node<int> *node){s+=node->value(); int Result(){return s; private: int s; 696

697 ; class Maxsearch: public Action<int>{ public: Maxsearch(){l = false; void Exec(Node<int> *node){ if(node->isleaf()){ if(!l){ l = true; max = node->value(); else if(node->value()>max) max = node->value(); bool Found(){return l; int MaxValue(){return max; private: int max; bool l; ; 697

698 int main() { BinTree<int> t; int i; while(cin >> i) { t.randominsert(i); Printer<int> print(cout); cout << "Preorder bejárás:"; t.preorder(&print); cout << endl; cout << "Inorder bejárás:"; t.inorder(&print); cout << endl; cout << "Postorder bejárás:"; t.postorder(&print); cout << endl; Summation sum; t.preorder(&sum); 698

699 cout << "Fa elemeinek összege:" << sum.result() << endl; Maxsearch ms; t.preorder(&ms); cout << "Maximum of internal elements:\n"; if(ms.found()) cout << ms.maxvalue() << endl; else cout << "none" << endl; return 0; 699

700 bintree.hpp: #ifndef BINTREE_H #define BINTREE_H #include <cstdlib> #include <time.h> template < typename Item> class Node { public: Item Value() const {return val; virtual bool IsLeaf() const = 0; bool IsInternal() const {return!isleaf(); protected: Node(const Item &v): val(v){ Item val; ; template < typename Item> class Action{ public: virtual void Exec(Node<Item> *node)=0; 700

701 ; template < typename Item> class BinTree{ public: BinTree():root(NULL){srand(time(NULL)); virtual ~BinTree(); void RandomInsert(const Item& e); void PreOrder (Action<Item> *todo) {Pre (root, todo); void InOrder (Action<Item> *todo) {In (root, todo); void PostOrder(Action<Item> *todo) {Post(root, todo); protected: LinkedNode<Item> *root; void Pre(LinkedNode<Item> *r,action<item>*todo); void In(LinkedNode<Item> *r, Action<Item>*todo); void Post(LinkedNode<Item> *r,action<item>*todo); ; BinTree(const BinTree&); BinTree& operator=(const BinTree&); 701

702 template < typename Item> BinTree<Item>::~BinTree() { DelAction del; Post(root, &del); template < typename Item> void BinTree<Item>::RandomInsert(const Item& e) { if(null == root) root = new LinkedNode<Item>(e,NULL,NULL); else { LinkedNode<Item> *r = root; int d = rand(); while(d&1? r->left!=null : r->right!=null){ if(d&1) r = r->left; else r = r->right; d = rand(); if(d&1) r->left = new LinkedNode<Item>(e,NULL,NULL); else r->right = 702

703 new LinkedNode<Item>(e,NULL,NULL); template < typename Item> void BinTree<Item>:: Pre(LinkedNode *r,action<item> *todo) { if(null == r) return; todo->exec(r); Pre(r->left, todo); Pre(r->right, todo); template < typename Item> void BinTree<Item>:: In(LinkedNode *r,action<item> *todo) { if(null == r) return; In(r->left, todo); todo->exec(r); In(r->right, todo); 703

704 template < typename Item> void BinTree<Item>:: Post(LinkedNode *r,action<item> *todo) { if(null == r) return; Post(r->left, todo); Post(r->right, todo); todo->exec(r); #endif 704

705 C++ kislexikon absztrakt osztály class O{ // vagy privát konstruktor private: Osztaly(); // vagy absztrakt metódus void Method() = 0; ; védett tag publikus származtatás privát származtatás virtuális metódus dinamikus kötés protected class O : public Os { ; class O : private Os { ; virtual void Method(); class O : public OS { void M(); ; OS *p = new O(); p->m(); osztály-sablon függvény-sablon operátor-sablon példányosítás template<typename T> class O { T ; template<typename T> void Fv( T ) { T template<typename T> void operator() { T O<int> o; 705

706 706 Fv<int>( )

707 15. Egy osztály-sablon könyvtár felhasználása Ebben a fejezetben egy esettanulmányt találunk, amely egy osztály-sablon könyvtárat és annak felhasználását mutatja be. A könyvtár a visszavezetéssel tervezett programok C++-beli megvalósítását támogatja, és arra a programozói stílusra támaszkodik, amely származtatással, a virtuális metódusok felüldefiniálásával, valamint osztály-sablonok példányosításával éri el egy már megírt kód újrahasznosítását. A célunk az, hogy a felsorolóra megfogalmazott programozási tételeket, helyesebben az arra visszavezetett programrészeket egy-egy tevékenység-objektumként hajtsuk végre, egészen pontosan a tevékenységobjektum Run() metódusának meghívásával. A programozási tételeket a lehető legáltalánosabb formában egy-egy osztály-sablonba ágyazva kódoljuk, és ebből (fordítási és futási időben) példányosítjuk-származtatjuk a konkrét tevékenység-objektumok osztályát. A virtuális metódusok felüldefiniálásával adhatjuk majd meg a programozási tétel speciális feltételeit (ha szükség van erre), a sablon paraméterek segítségével állíthatjuk be például azt, hogy mi a tevékenység által feldolgozott elemeknek a típusa és a tevékenységobjektumnak futás közben adjuk át azt a felsoroló-objektumot, amely adagolja majd a feldolgozandó elemeket a tevékenység számára. Nemcsak a könyvtár felhasználása épül az objektum orientált technológiára, de maga a könyvtár is ennek szellemében készült. Például azt a feldolgozási stratégiát, amelyet mindegyik nevezetes programozási tétel követ: nevezetesen, hogy végig kell menni egy felsoroló (legyen ennek a neve mondjuk enor) által előállított elemeken és azokat kell feldolgozni, általánosan írtuk le egy ősosztály-sablon Run() metódusában, amely majd származtatás révén használhatnak fel az egyes programozási tételek. Init(); for (enor.first();!enor.end(); enor.next()) { Do(enor.Current()); 707

708 A bemutatott osztály-sablon könyvtár nem az ipari alkalmazások számára készült. Nem hisszük, hogy a gyakorlatban való felhasználása egyszerű illetve célszerű lenne. Egy programozási tétel (lényegében egy ciklus) implementálása ugyanis önmagában nem túl nehéz feladat, ezért sokkal könnyebb közvetlenül kódolni, mint egy összetett osztály-sablon könyvtárból származtatni, hiszen ehhez a könyvtár elemeit kell pontosan megismerni és helyesen alkalmazni. Ennél fogva ez a tanulmány rávilágít arra a határra is, hogy mikor érdemes az objektum-orientált illetve generikus nyelvi eszközöket bevetni egy feladat csoport megoldásánál és ez mikor nem jelent már előnyt. Ugyanakkor ez a könyvtár nagyon is alkalmas a különféle objektum-orientált implementációs technikák megmutatására. A könyvtár használatával igen szép megoldásokat tudunk előállítani. Programozói szemmel például nagyon érdekes, hogy az előállított megoldásokban mindössze egyetlen ciklus lesz, mégpedig a programozási tételek ősosztálysablonjának előbb említett Run() metódusában, az alkalmazás során hozzáadott kódban pedig egyáltalán nem kell majd ciklust írni. Osztály-sablon könyvtár tervezése Tekintsük át most részleteiben az osztály-könyvtár elemeit. (Az osztályok tagjai előtt álló + jel a publikus, a # jel a védett tagokat jelöli. A dőlt betűvel szedett elemek az absztrakt elemek.) A felsorolók általános tulajdonságait az Enumerator absztrakt osztálysablonban rögzítjük ábra. Az felsorolók absztrakt osztály-sablon 708

709 Minden olyan objektum, amelynek osztálya ebből származik, rendelkezni fog a bejárás itt bevezetett négy alapműveletével. Ezek a műveletek ezen a szinten nincsenek definiálva (absztraktak), a bejárt elemek típusát pedig az Item sablonparaméter jelzi. E fejezetben megoldott feladatok bemenő adatait egy szöveges állomány tartalmazza, amelyet szekvenciális inputfájlként kívánunk feldolgozni. Ezért célszerű kiegészíteni az osztály-sablon könyvtárat egy olyan felsoroló osztállyal, amelynek objektumai szöveges állományra épített szekvenciális inputfájl elemeit képesek sorban egymás után végigolvasni (bejárni) ábra. Szöveges állomány felsorolójának osztály-sablonja A First() és a Next() a soron következő elemet olvassák be a df változóba, ennek értékét kérdezi le a Current(), és ha a legutolsó olvasás sikertelen volt, akkor az End() igazat fog visszaadni. Habár olyan feladatot nem fogunk most látni, ahol egy tömb elemeit kell feldolgozni, ez azért gyakori eset, ezért érdemes a könyvtárba felvenni az egy-dimenziós tömb elemeit felsoroló osztályt. 709

710 15-3. ábra. Tömb felsorolójának osztály-sablon Ennek reprezentációja a tömb mellett egy indexváltozót is tartalmaz, amelyet a First() művelet állít rá a tömb első elemére, a Next() növeli meg eggyel az értékét, a Current() a tömb ennyiedik elemét adja vissza és az End() akkor jelez majd igazat, ha az index túlhaladt a tömb végén. A Procedure osztály-sablon a központi eleme a könyvtárnak. Minden programozási tételnek ez az őse. Egyfelől definiál egy olyan metódust (AddEnumerator), amellyel egy konkrét felsorolót (enor) lehet a feldolgozáshoz hozzákapcsolni, másfelől tartalmazza ezen felsoroló által bejárt elemeknek a bevezetőben már bemutatott feldolgozását végző Run() metódust. A Run() közvetve vagy közvetlenül több olyan metódust is meghív, amelyeket majd a származtatás során lehet vagy kell felüldefiniálni. Ezek között az Init() és a Do() absztrakt metódusok, a többi rendelkezik alapértelmezett működéssel. 710

711 15-4. ábra. A programozási tételek ősosztály-sablonja A Run() metódus végleges változata néhány részletében eltér a bevezetőben vázolt verzióhoz képest. Egyrészt a felsoroló enor.first() metódusa helyett egy olyan First() metódus hívását találjuk benne, amelynek alapértelmezett definíciója éppen az enor.first() lesz, de ez szükség esetén felülbírálható. Ez akkor hasznos, ha egy tevékenységet olyan felsorolóval kell elvégezni, amelyet már korábban használtunk, de félbe hagytuk, és most folytatni akarjuk a felsorolást. Ilyenkor nincs szükség újra az enor.first() végrehajtására, ezért a First() metódust ilyenkor az üres utasítással definiáljuk felül. Másrészt a!enor.end()ciklusfeltételt kibővítjük (szigorítjuk) egy WhileCond()metódus hívásával, amely alapértelmezés szerint mindig igaz értéket ad vissza (azaz nem vezet be megszorítást), de ha kell, felüldefiniálható. Ezzel azt érhetjük el, hogy a feldolgozás még az előtt álljon le, mielőtt a felsorolás véget érne. Sokszor kell ugyanis egy programozási tételt úgy használni, hogy az a felsorolás vége előtt egy speciális feltétel bekövetkezésekor véget érjen. (Például adjuk össze egy sorozat számait, de csak az első negatív szám előttieket.) A WhileCond() metódus értéke a felsorolás aktuális elemétől függ: amíg ez az elem kielégíti az itt megadott feltételt, addig folytatódhat a feldolgozás. Harmadrészt az így kibővített ciklusfeltételt a LoopCond() metódusban fogjuk össze, hogy ez által lehetővé tegyük a ciklusfeltétel későbbi módosítását (mint ahogy ezt a lineáris keresés és a kiválasztás megfogalmazásánál meg is tesszük majd). Init(); for (First(); LoopCond(); enor.next()) { Do(enor.Current()); A Procedure osztályból származtatjuk a programozási tételek osztályait. 711

712 Az összegzés tételével többféle feladat-típust meg lehet oldani. A szigorúan vett összegzés mellett ilyen például az összeszorzás, összefűzés, feltételes összegzés, számlálás, másolás, kiválogatás, szétválogatás és összefuttatás. Ezt az általánosságot tükrözi a Summation osztály-sablon. Az Item sablonparaméter a feldolgozandó elemek típusára, a ResultType paraméter az összegzés eredményének típusára utal. A ResultType típusú result adattag az eredmény tárolására szolgál, amelyet majd a Result() metódussal kérdezhetünk le ábra. Az összegzés osztály-sablonja Az osztály egy feltételes tevékenység formájában implementálja a Do() metódust ( if (Cond(e)) Add(e) ), ahol az e az éppen felsorolt elem, amelyet a Do() metódus paramétereként megkap. A Cond() (itt csak az alapértelmezés szerinti megvalósítását adhatjuk meg, amely mindig igazat ad) ezen elem alapján ad vissza egy logikai értéket, amely ha igaz, akkor az Add() metódus ezen a szinten ez is absztrakt ugyancsak ezen elem alapján módosíthatja az eredményt. Az eredményt a konkrét felhasználáskor az ősosztály absztrakt Init() metódusának felüldefiniálásával kell majd inicializálni. Talán jobban megértjük az összegzés osztály-sablonját, ha származtatjuk belőle a számlálás programozási tételét leíró osztályt. Ez egy speciális összegzés, amennyiben a ResultType típus ilyenkor az egész számok típusa lesz, a result adattagot az Init() metódusban nulla kezdőértékre kell beállítani, és ezt az értéket az Add() metódusban kell 712

713 eggyel növelni. Az általános számlálásnál a Cond() nem kap új jelentést, viszont egy konkrét számlálásnál ezt kell majd felüldefiniálni ábra. A számlálás osztály-sablonja A MaxSearch osztály-sablon az általános maximumkeresést definiálja, amelyből egy közönséges maximum kiválasztás éppen úgy származtatható, mint egy feltételes maximumkeresés. Mivel a Procedure osztály-sablonból származtatjuk, ezért elsődleges feladata, hogy véglegesen implementálja a Do() és az Init() metódust. Sablon-paraméterei között találjuk a feldolgozandó elemek típusát (Item), az összehasonlítandó értékek típusát (Value, ami sokszor megegyezik az Item -mel) és az összehasonlítás típusát (Compare). A Compare helyére olyan típust kell tennünk, amely lehetővé teszi, hogy annak egy objektuma két Value típusú objektumot legyen képes összehasonlítani, és eldönteni melyik a jobb. Az l logikai típusú adattag jelzi majd, hogy találtunk-e megfelelő tulajdonságú elemet, az ilyenek közt talált legjobb elemet az Item típusú optitem adattag őrzi, amelynek értéke a Value típusú opt adattagba kerül, a Compare típusú összehasonlító objektum pedig a better adattag lesz. Ezek mind védett adattagok, az első három értékének lekérdezését az osztály-sablon Found(), Opt() és OptItem() publikus metódusai biztosítják. 713

714 15-7. ábra. Az általános maximumkeresés osztály-sablonja A Do() a feltételes maximumkeresés programozási tételéből ismert hármas elágazást mutatja. Az Init() felüldefiniálásában a feltételes maximumkeresésben is szereplő l logikai változót állítjuk be hamisra. A Func() a maximumkeresés programozási tételében bevezetett f függvény, amely egy elemhez azt az értéket rendeli, amely szerint az elemeket össze kell hasonlítani, de ezen az általános szinten még nem lehet definiálni, ezért absztrakt. A Cond()a keresési feltételt írja le, alapértelmezett jelentése igaz (ha ezt megtartjuk, akkor az általános maximumkeresés egy közönséges maximum kiválasztás lesz). A kiválasztás osztály-sablonja implementálja az Init()és a Do() metódusokat. valamint felülírja a LoopCond()metódust. 714

715 15-8. ábra. A kiválasztás osztály-sablonja Sajátos módon az Init()és a Do() metódus az üres utasítás lesz. A LoopCond() metódust úgy kell felüldefiniálni, hogy éppen annak az absztrakt Cond() metódusnak a tagadottja legyen, amelynek egy konkrét kiválasztási feladatnál történő felüldefiniálásával a keresett elem tulajdonságát írhatjuk le. A lineáris keresés osztály-sablonja biztosítja mind a normális (pesszimista), mind a tagadott (optimista) lineáris keresés előállítását. A pesszimista ( úgy sem fogunk találni megfelelő elemet, de próbálkozzunk ) lineáris keresés egy felsorolásnak az első adott tulajdonságú elemét (elem) keresi meg. Egyfelől megmondja, hogy talált-e egyáltalán ilyet (l), és ha igen tárolja az elsőt (elem). Az optimista ( nyilván minden elem megadott tulajdonságú, de a biztonság kedvéért nézzük át őket ) lineáris keresés azt dönti el, hogy a felsorolás minden eleme rendelkezik-e a megadott tulajdonsággal (l), és ha nem, az első nem ilyen tulajdonságút adja meg (elem). A keresés eredménye a Found() és Elem() publikus metódusokkal kérdezhető le. A Do() metódust a lineáris keresések ciklusmagjának megfelelően implementáljuk, amely a β feltételt (lásd előző kötet) helyettesítő Cond() metódust hívja. A metódus az l logikai változót igaz-ra állítja, ha az aktuális elem kielégíti a keresett tulajdonságot és ekkor az elem az aktuális elemet kapja értékül, különben az l hamis lesz. 715

716 15-9. ábra. A lineáris keresés osztály-sablonja Azt, hogy a keresés pesszimista vagy optimista legyen, egy külön sablon-paraméterrel állíthatjuk be. Alapértelmezés szerint ez a paraméter hamis, amely a pesszimista lineáris keresést definiálja, mert ennél kezdetben hamis értéket adunk a keresés logikai változójának. Az optimista lineáris keresést a sablon-paraméter igaz értéke jelzi, mert ekkor a logikai változót a keresés elején igaz-ra kell állítani. A keresést tehát a sablon-paraméter értékétől függően kell inicializálni (Init()), de a ciklusfeltétele (LoopCond()) is ennek megfelelően változik. Ezért mindkettőt felül kell definiálnunk. A keresett tulajdonságot a Cond() absztrakt metódus adja, amelyet majd a konkrét kereséseknél kell felüldefiniálni. Végezetül tekintsük át a teljes osztály könyvtárat az osztályok közötti kapcsolatokkal együtt. A programozási tételeket leíró osztályok véglegesen definiálják a Do() metódust, és az összegzés kivételével az Init() metódust is. Véglegesek a publikus metódusok, a számlálásnál az Add(), a kiválasztásnál és a lineáris keresésnél a LoopCond(). 716

717 Item Item Item Item Item,ResultType Item,Value,Compare Item Item,optimist Item Item Item ábra. Programozási tételek osztály-sablon kód-könyvtára Osztály-sablon könyvtár implementálása A fent bevezetett osztály-sablonokat C++ nyelven készítjük el. Az absztrakt felsoroló ősosztály sablonja a bejáró műveletek absztrakt virtuális (tehát kötelezően felüldefiniálandó) metódusait írja le. 717

Programozási alapismeretek 1. előadás

Programozási alapismeretek 1. előadás Programozási alapismeretek 1. előadás Tartalom A problémamegoldás lépései programkészítés folyamata A specifikáció Az algoritmus Algoritmikus nyelvek struktogram A kódolás a fejlesztői környezet 2/33 A

Részletesebben

PROGRAMOZÁS tantárgy. Gregorics Tibor egyetemi docens ELTE Informatikai Kar

PROGRAMOZÁS tantárgy. Gregorics Tibor egyetemi docens ELTE Informatikai Kar PROGRAMOZÁS tantárgy Gregorics Tibor egyetemi docens ELTE Informatikai Kar Követelmények A,C,E szakirány B szakirány Előfeltétel Prog. alapismeret Prog. alapismeret Diszkrét matematika I. Óraszám 2 ea

Részletesebben

Programozás Minta programterv a 1. házi feladathoz 1.

Programozás Minta programterv a 1. házi feladathoz 1. Programozás Minta programterv a 1. házi feladathoz 1. Gregorics Tibor 1. beadandó/0.feladat 2008. december 6. EHACODE.ELTE gt@inf.elte.hu 0.csoport Feladat Egy osztályba n diák jár, akik m darab tantárgyat

Részletesebben

Bevezetés az informatikába

Bevezetés az informatikába Bevezetés az informatikába 6. előadás Dr. Istenes Zoltán Eötvös Loránd Tudományegyetem Informatikai Kar Programozáselmélet és Szoftvertechnológiai Tanszék Matematikus BSc - I. félév / 2008 / Budapest Dr.

Részletesebben

Maximum kiválasztás tömbben

Maximum kiválasztás tömbben ELEMI ALKALMAZÁSOK FEJLESZTÉSE I. Maximum kiválasztás tömbben Készítette: Szabóné Nacsa Rozália Gregorics Tibor tömb létrehozási módozatok maximum kiválasztás kódolása for ciklus adatellenőrzés do-while

Részletesebben

Programtervezés. Dr. Iványi Péter

Programtervezés. Dr. Iványi Péter Programtervezés Dr. Iványi Péter 1 A programozás lépései 2 Feladat meghatározás Feladat kiírás Mik az input adatok A megoldáshoz szükséges idő és költség Gyorsan, jót, olcsón 3 Feladat megfogalmazása Egyértelmű

Részletesebben

Gregorics Tibor Tanácsok modularizált programok készítéséhez 1

Gregorics Tibor Tanácsok modularizált programok készítéséhez 1 Gregorics Tibor Tanácsok modularizált programok készítéséhez 1 Modularizált programon azt értjük, amely több, jól körülhatárolható részfeladat megoldásaiból épül fel. Egy-egy részfeladat gyakran szabványos

Részletesebben

Programozási alapismeretek. 1. előadás. A problémamegoldás lépései. A programkészítés folyamata. Az algoritmus fogalma. Nyelvi szintek.

Programozási alapismeretek. 1. előadás. A problémamegoldás lépései. A programkészítés folyamata. Az algoritmus fogalma. Nyelvi szintek. Tartalom 1. előadás programozás során használt nyelvek A specifikáció Algoritmikus nyelvek A problémamegoldás lépései 3/41 (miből?, mit?) specifikáció (mivel?, hogyan?) adat- + algoritmus-leírás 3. (a

Részletesebben

HORVÁTH ZSÓFIA 1. Beadandó feladat (HOZSAAI.ELTE) ápr 7. 8-as csoport

HORVÁTH ZSÓFIA 1. Beadandó feladat (HOZSAAI.ELTE) ápr 7. 8-as csoport 10-es Keressünk egy egész számokat tartalmazó négyzetes mátrixban olyan oszlopot, ahol a főátló alatti elemek mind nullák! Megolda si terv: Specifika cio : A = (mat: Z n m,ind: N, l: L) Ef =(mat = mat`)

Részletesebben

Bevezetés a programozásba I.

Bevezetés a programozásba I. Bevezetés a programozásba I. 6. gyakorlat C++ alapok, szövegkezelés Surányi Márton PPKE-ITK 2010.10.12. Forrásfájlok: *.cpp fájlok Fordítás: a folyamat, amikor a forrásfájlból futtatható állományt állítunk

Részletesebben

Programozási alapismeretek beadandó feladat: ProgAlap beadandó feladatok téma 99. feladat 1

Programozási alapismeretek beadandó feladat: ProgAlap beadandó feladatok téma 99. feladat 1 Programozási alapismeretek beadandó feladat: ProgAlap beadandó feladatok téma 99. feladat 1 Készítette: Gipsz Jakab Neptun-azonosító: A1B2C3 E-mail: gipszjakab@vilaghalo.hu Kurzuskód: IP-08PAED Gyakorlatvezető

Részletesebben

1. Alapok. #!/bin/bash

1. Alapok. #!/bin/bash 1. oldal 1.1. A programfájlok szerkezete 1. Alapok A bash programok tulajnképpen egyszerű szöveges fájlok, amelyeket bármely szövegszerkesztő programmal megírhatunk. Alapvetően ugyanazokat a at használhatjuk

Részletesebben

Occam 1. Készítette: Szabó Éva

Occam 1. Készítette: Szabó Éva Occam 1. Készítette: Szabó Éva Párhuzamos programozás Egyes folyamatok (processzek) párhuzamosan futnak. Több processzor -> tényleges párhuzamosság Egy processzor -> Időosztásos szimuláció Folyamatok közötti

Részletesebben

Algoritmizálás, adatmodellezés tanítása 6. előadás

Algoritmizálás, adatmodellezés tanítása 6. előadás Algoritmizálás, adatmodellezés tanítása 6. előadás Tesztelési módszerek statikus tesztelés kódellenőrzés szintaktikus ellenőrzés szemantikus ellenőrzés dinamikus tesztelés fekete doboz módszerek fehér

Részletesebben

Vezérlési szerkezetek

Vezérlési szerkezetek Vezérlési szerkezetek Szelekciós ok: if, else, switch If Segítségével valamely ok végrehajtását valamely feltétel teljesülése esetén végezzük el. Az if segítségével valamely tevékenység () végrehajtását

Részletesebben

Webprogramozás szakkör

Webprogramozás szakkör Webprogramozás szakkör Előadás 5 (2012.04.09) Programozás alapok Eddig amit láttunk: Programozás lépései o Feladat leírása (specifikáció) o Algoritmizálás, tervezés (folyamatábra, pszeudokód) o Programozás

Részletesebben

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

Programozás II. 2. Dr. Iványi Péter Programozás II. 2. Dr. Iványi Péter 1 C++ Bjarne Stroustrup, Bell Laboratórium Első implementáció, 1983 Kezdetben csak precompiler volt C++ konstrukciót C-re fordította A kiterjesztés alapján ismerte fel:.cpp.cc.c

Részletesebben

Feladat. Bemenő adatok. Bemenő adatfájlok elvárt formája. Berezvai Dániel 1. beadandó/4. feladat 2012. április 13. Például (bemenet/pelda.

Feladat. Bemenő adatok. Bemenő adatfájlok elvárt formája. Berezvai Dániel 1. beadandó/4. feladat 2012. április 13. Például (bemenet/pelda. Berezvai Dániel 1. beadandó/4. feladat 2012. április 13. BEDTACI.ELTE Programozás 3ice@3ice.hu 11. csoport Feladat Madarak életének kutatásával foglalkozó szakemberek különböző településen különböző madárfaj

Részletesebben

Változók. Mennyiség, érték (v. objektum) szimbolikus jelölése, jelentése Tulajdonságai (attribútumai):

Változók. Mennyiség, érték (v. objektum) szimbolikus jelölése, jelentése Tulajdonságai (attribútumai): Javascript Változók Mennyiség, érték (v. objektum) szimbolikus jelölése, jelentése Tulajdonságai (attribútumai): Név Érték Típus Memóriacím A változó értéke (esetleg más attribútuma is) a program futása

Részletesebben

1. Jelölje meg az összes igaz állítást a következők közül!

1. Jelölje meg az összes igaz állítást a következők közül! 1. Jelölje meg az összes igaz állítást a következők közül! a) A while ciklusban a feltétel teljesülése esetén végrehajtódik a ciklusmag. b) A do while ciklusban a ciklusmag után egy kilépési feltétel van.

Részletesebben

Változók. Mennyiség, érték (v. objektum) szimbolikus jelölése, jelentése Tulajdonságai (attribútumai):

Változók. Mennyiség, érték (v. objektum) szimbolikus jelölése, jelentése Tulajdonságai (attribútumai): Python Változók Mennyiség, érték (v. objektum) szimbolikus jelölése, jelentése Tulajdonságai (attribútumai): Név Érték Típus Memóriacím A változó értéke (esetleg más attribútuma is) a program futása alatt

Részletesebben

Programozás alapjai gyakorlat. 2. gyakorlat C alapok

Programozás alapjai gyakorlat. 2. gyakorlat C alapok Programozás alapjai gyakorlat 2. gyakorlat C alapok 2016-2017 Bordé Sándor 2 Forráskód, fordító, futtatható állomány Először megírjuk a programunk kódját (forráskód) Egyszerű szövegszerkesztőben vagy fejlesztőkörnyezettel

Részletesebben

Bevezetés a programozásba. 8. Előadás: Függvények 2.

Bevezetés a programozásba. 8. Előadás: Függvények 2. Bevezetés a programozásba 8. Előadás: Függvények 2. ISMÉTLÉS Helló #include using namespace std; int main() cout

Részletesebben

Algoritmizálás és adatmodellezés tanítása beadandó feladat: Algtan1 tanári beadandó /99 1

Algoritmizálás és adatmodellezés tanítása beadandó feladat: Algtan1 tanári beadandó /99 1 Algoritmizálás és adatmodellezés tanítása beadandó feladat: Algtan1 tanári beadandó /99 1 Készítette: Gipsz Jakab Neptun-azonosító: ABC123 E-mail: gipszjakab@seholse.hu Kurzuskód: IT-13AAT1EG 1 A fenti

Részletesebben

Programozás C++ -ban 2007/1

Programozás C++ -ban 2007/1 Programozás C++ -ban 2007/1 1. Különbségek a C nyelvhez képest Több alapvető különbség van a C és a C++ programozási nyelvek szintaxisában. A programozó szempontjából ezek a különbségek könnyítik a programozó

Részletesebben

Szerző. Varga Péter ETR azonosító: VAPQAAI.ELTE Email cím: Név: vp.05@hotmail.com Kurzuskód:

Szerző. Varga Péter ETR azonosító: VAPQAAI.ELTE Email cím: Név: vp.05@hotmail.com Kurzuskód: Szerző Név: Varga Péter ETR azonosító: VAPQAAI.ELTE Email cím: vp.05@hotmail.com Kurzuskód: IP-08PAEG/27 Gyakorlatvezető neve: Kőhegyi János Feladatsorszám: 20 1 Tartalom Szerző... 1 Felhasználói dokumentáció...

Részletesebben

A PiFast program használata. Nagy Lajos

A PiFast program használata. Nagy Lajos A PiFast program használata Nagy Lajos Tartalomjegyzék 1. Bevezetés 3 2. Bináris kimenet létrehozása. 3 2.1. Beépített konstans esete.............................. 3 2.2. Felhasználói konstans esete............................

Részletesebben

OEP Gregorics Tibor: Minta dokumentáció a 3. házi feladathoz 1. Feladat. Elemzés 1

OEP Gregorics Tibor: Minta dokumentáció a 3. házi feladathoz 1. Feladat. Elemzés 1 OEP Gregorics Tibor: Minta dokumentáció a 3. házi feladathoz 1. Feladat Különféle élőlények egy túlélési versenyen vesznek részt. A lények egy pályán haladnak végig, ahol váltakozó terep viszonyok vannak.

Részletesebben

9. előadás. Programozás-elmélet. Programozási tételek Elemi prog. Sorozatszámítás Eldöntés Kiválasztás Lin. keresés Megszámolás Maximum.

9. előadás. Programozás-elmélet. Programozási tételek Elemi prog. Sorozatszámítás Eldöntés Kiválasztás Lin. keresés Megszámolás Maximum. Programozási tételek Programozási feladatok megoldásakor a top-down (strukturált) programtervezés esetén három vezérlési szerkezetet használunk: - szekvencia - elágazás - ciklus Eddig megismertük az alábbi

Részletesebben

Szoftvertervezés és -fejlesztés I.

Szoftvertervezés és -fejlesztés I. Szoftvertervezés és -fejlesztés I. Operátorok Vezérlési szerkezetek Gyakorlás 1 Hallgatói Tájékoztató A jelen bemutatóban található adatok, tudnivalók és információk a számonkérendő anyag vázlatát képezik.

Részletesebben

INFORMATIKA javítókulcs 2016

INFORMATIKA javítókulcs 2016 INFORMATIKA javítókulcs 2016 ELMÉLETI TÉTEL: Járd körbe a tömb fogalmát (Pascal vagy C/C++): definíció, egy-, két-, több-dimenziós tömbök, kezdőértékadás definíciókor, tömb típusú paraméterek átadása alprogramoknak.

Részletesebben

3. Ezután a jobb oldali képernyő részen megjelenik az adatbázistábla, melynek először a rövid nevét adjuk meg, pl.: demo_tabla

3. Ezután a jobb oldali képernyő részen megjelenik az adatbázistábla, melynek először a rövid nevét adjuk meg, pl.: demo_tabla 1. Az adatbázistábla létrehozása a, Ha még nem hoztunk létre egy adatbázistáblát sem, akkor a jobb egérrel a DDIC-objekt. könyvtárra kattintva, majd a Létrehozás és az Adatbázistábla menüpontokat választva

Részletesebben

Regionális forduló november 18.

Regionális forduló november 18. Regionális forduló 2017. november 18. 9-10. osztályosok feladata Feladat Egy e-mail kliens szoftver elkészítése lesz a feladatotok. Az elkészítendő alkalmazásnak az alábbiakban leírt specifikációnak kell

Részletesebben

Java programozási nyelv

Java programozási nyelv Java programozási nyelv 2. rész Vezérlő szerkezetek Nyugat-Magyarországi Egyetem Faipari Mérnöki Kar Informatikai Intézet Soós Sándor 2005. szeptember A Java programozási nyelv Soós Sándor 1/23 Tartalomjegyzék

Részletesebben

A KÓDOLÁS TECHNIKAI ELVEI

A KÓDOLÁS TECHNIKAI ELVEI 1. A KÓDOLÁS FOGALMA A KÓDOLÁS TECHNIKAI ELVEI A kódolás a forrásnyelvű (pl. C#, Java) program elkészítését jelenti. Ha a megoldást gondosan megterveztük, akkor ez általában már csak rutinszerű, technikai

Részletesebben

Bevezetés a programozásba I 4. gyakorlat. PLanG: Szekvenciális fájlkezelés. Szekvenciális fájlkezelés Fájlok használata

Bevezetés a programozásba I 4. gyakorlat. PLanG: Szekvenciális fájlkezelés. Szekvenciális fájlkezelés Fájlok használata Pázmány Péter Katolikus Egyetem Információs Technológiai Kar Bevezetés a programozásba I 4. gyakorlat PLanG: 2011.10.04. Giachetta Roberto groberto@inf.elte.hu http://people.inf.elte.hu/groberto Fájlok

Részletesebben

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

Programozás alapjai C nyelv 8. gyakorlat. Mutatók és címek (ism.) Indirekció (ism) Programozás alapjai C nyelv 8. gyakorlat Szeberényi Imre BME IIT Programozás alapjai I. (C nyelv, gyakorlat) BME-IIT Sz.I. 2005.11.07. -1- Mutatók és címek (ism.) Minden változó és függvény

Részletesebben

Bevezetés a programozásba

Bevezetés a programozásba Bevezetés a programozásba 1. Előadás Bevezetés, kifejezések http://digitus.itk.ppke.hu/~flugi/ Egyre precízebb A programozás természete Hozzál krumplit! Hozzál egy kiló krumplit! Hozzál egy kiló krumplit

Részletesebben

1. Egyszerű (primitív) típusok. 2. Referencia típusok

1. Egyszerű (primitív) típusok. 2. Referencia típusok II. A Java nyelv eszközei 1. Milyen eszközöket nyújt a Java a programozóknak Korábban már említettük, hogy a Java a C nyelvből alakult ki, ezért a C, C++ nyelvben járatos programozóknak nem fog nehézséget

Részletesebben

Felvételi tematika INFORMATIKA

Felvételi tematika INFORMATIKA Felvételi tematika INFORMATIKA 2016 FEJEZETEK 1. Természetes számok feldolgozása számjegyenként. 2. Számsorozatok feldolgozása elemenként. Egydimenziós tömbök. 3. Mátrixok feldolgozása elemenként/soronként/oszloponként.

Részletesebben

Tömbök kezelése. Példa: Vonalkód ellenőrzőjegyének kiszámítása

Tömbök kezelése. Példa: Vonalkód ellenőrzőjegyének kiszámítása Tömbök kezelése Példa: Vonalkód ellenőrzőjegyének kiszámítása A számokkal jellemzett adatok, pl. személyi szám, adószám, taj-szám, vonalkód, bankszámlaszám esetében az elírásból származó hibát ún. ellenőrző

Részletesebben

Tartalomjegyzék. Általános Információ! 2. Felhasználói dokumentáció! 3. Feladat! 3. Környezet! 3. Használat! 3. Bemenet! 3. Példa!

Tartalomjegyzék. Általános Információ! 2. Felhasználói dokumentáció! 3. Feladat! 3. Környezet! 3. Használat! 3. Bemenet! 3. Példa! Tartalomjegyzék Általános Információ! 2 Felhasználói dokumentáció! 3 Feladat! 3 Környezet! 3 Használat! 3 Bemenet! 3 Példa! 3 A program eredménye! 3 Példa! 3 Hibalehetőségek! 3 Példa! 3 Fejlesztői dokumentáció!

Részletesebben

A C# programozási nyelv alapjai

A C# programozási nyelv alapjai A C# programozási nyelv alapjai Tisztán objektum-orientált Kis- és nagybetűket megkülönbözteti Ötvözi a C++, Delphi, Java programozási nyelvek pozitívumait.net futtatókörnyezet Visual Studio fejlesztőkörnyezet

Részletesebben

SZÁMRENDSZEREK KÉSZÍTETTE: JURÁNYINÉ BESENYEI GABRIELLA

SZÁMRENDSZEREK KÉSZÍTETTE: JURÁNYINÉ BESENYEI GABRIELLA SZÁMRENDSZEREK KÉSZÍTETTE: JURÁNYINÉ BESENYEI GABRIELLA BINÁRIS (kettes) ÉS HEXADECIMÁLIS (tizenhatos) SZÁMRENDSZEREK (HELYIÉRTÉK, ÁTVÁLTÁSOK, MŰVELETEK) A KETTES SZÁMRENDSZER A computerek világában a

Részletesebben

5. Gyakorlat. struct diak {

5. Gyakorlat. struct diak { Rövid elméleti összefoglaló 5. Gyakorlat Felhasználó által definiált adattípusok: A typedef egy speciális tárolási osztály, mellyel érvényes típusokhoz szinonim nevet rendelhetünk. typedef létező_típus

Részletesebben

1. Alapok. Programozás II

1. Alapok. Programozás II 1. Alapok Programozás II Elérhetőség Név: Smidla József Elérhetőség: smidla dcs.uni-pannon.hu Szoba: I916 2 Irodalom Bjarne Stroustrup: A C++ programozási nyelv 3 Irodalom Erich Gamma, Richard Helm, Ralph

Részletesebben

Bevezetés a programozásba I 4. gyakorlat. PLanG: Szekvenciális fájlkezelés

Bevezetés a programozásba I 4. gyakorlat. PLanG: Szekvenciális fájlkezelés Pázmány Péter Katolikus Egyetem Információs Technológiai Kar Bevezetés a programozásba I 4. gyakorlat PLanG: 2011.10.04. Giachetta Roberto groberto@inf.elte.hu http://people.inf.elte.hu/groberto Fájlok

Részletesebben

A programozás alapjai 1 Rekurzió

A programozás alapjai 1 Rekurzió A programozás alapjai Rekurzió. előadás Híradástechnikai Tanszék - preorder (gyökér bal gyerek jobb gyerek) mentés - visszaállítás - inorder (bal gyerek gyökér jobb gyerek) rendezés 4 5 6 4 6 7 5 7 - posztorder

Részletesebben

Programozás C és C++ -ban

Programozás C és C++ -ban Programozás C és C++ -ban 1. Különbségek a C nyelvhez képest Több alapvető különbség van a C és a C++ programozási nyelvek szintaxisában. A programozó szempontjából ezek a különbségek könnyítik a programozó

Részletesebben

Programozási segédlet

Programozási segédlet Programozási segédlet Programozási tételek Az alábbiakban leírtam néhány alap algoritmust, amit ismernie kell annak, aki programozásra adja a fejét. A lista korántsem teljes, ám ennyi elég kell legyen

Részletesebben

Rekurzió. Dr. Iványi Péter

Rekurzió. Dr. Iványi Péter Rekurzió Dr. Iványi Péter 1 Függvényhívás void f3(int a3) { printf( %d,a3); } void f2(int a2) { f3(a2); a2 = (a2+1); } void f1() { int a1 = 1; int b1; b1 = f2(a1); } 2 Függvényhívás void f3(int a3) { printf(

Részletesebben

Aritmetikai kifejezések lengyelformára hozása

Aritmetikai kifejezések lengyelformára hozása Aritmetikai kifejezések lengyelformára hozása Készítették: Santák Csaba és Kovács Péter, 2005 ELTE IK programtervező matematikus szak Aritmetikai kifejezések kiértékelése - Gyakran felmerülő programozási

Részletesebben

Már megismert fogalmak áttekintése

Már megismert fogalmak áttekintése Interfészek szenasi.sandor@nik.bmf.hu PPT 2007/2008 tavasz http://nik.bmf.hu/ppt 1 Témakörök Polimorfizmus áttekintése Interfészek Interfészek kiterjesztése Eseménykezelési módszerek 2 Már megismert fogalmak

Részletesebben

C programozási nyelv

C programozási nyelv C programozási nyelv Előfeldolgozó utasítások Dr Schuster György 2011 május 3 Dr Schuster György () C programozási nyelv Előfeldolgozó utasítások 2011 május 3 1 / 15 A fordítás menete Dr Schuster György

Részletesebben

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

Mutatók és címek (ism.) Programozás alapjai C nyelv 8. gyakorlat. Indirekció (ism) Néhány dolog érthetőbb (ism.) Változók a memóriában Programozás alapjai C nyelv 8. gyakorlat Szeberényi mre BME T Programozás alapjai. (C nyelv, gyakorlat) BME-T Sz.. 2005.11.07. -1- Mutatók és címek (ism.) Minden változó és függvény

Részletesebben

A C programozási nyelv I. Bevezetés

A C programozási nyelv I. Bevezetés A C programozási nyelv I. Bevezetés Miskolci Egyetem Általános Informatikai Tanszék A C programozási nyelv I. (bevezetés) CBEV1 / 1 A C nyelv története Dennis M. Ritchie AT&T Lab., 1972 rendszerprogramozás,

Részletesebben

Bevezetés a C++ programozási nyelvbe

Bevezetés a C++ programozási nyelvbe Bevezetés a C++ programozási nyelvbe Miskolci Egyetem Általános Informatikai Tanszék CPP0 / 1 Története A C++ programozási nyelv a C programozási nyelv objektum orientált kiterjesztése. Az ANSI-C nyelvet

Részletesebben

C programozás. 6 óra Függvények, függvényszerű makrók, globális és

C programozás. 6 óra Függvények, függvényszerű makrók, globális és C programozás 6 óra Függvények, függvényszerű makrók, globális és lokális változók 1.Azonosítók A program bizonyos összetevőire névvel (azonosító) hivatkozunk Első karakter: _ vagy betű (csak ez lehet,

Részletesebben

Programozás alapjai. (GKxB_INTM023) Dr. Hatwágner F. Miklós augusztus 29. Széchenyi István Egyetem, Gy r

Programozás alapjai. (GKxB_INTM023) Dr. Hatwágner F. Miklós augusztus 29. Széchenyi István Egyetem, Gy r Programozás alapjai (GKxB_INTM023) Széchenyi István Egyetem, Gy r 2019. augusztus 29. Feladat: írjuk ki az els 10 természetes szám négyzetét! #i n c l u d e i n t main ( v o i d ) { p r

Részletesebben

Programozás I. gyakorlat

Programozás I. gyakorlat Programozás I. gyakorlat 1. gyakorlat Alapok Eszközök Szövegszerkesztő: Szintaktikai kiemelés Egyszerre több fájl szerkesztése pl.: gedit, mcedit, joe, vi, Notepad++ stb. Fordító: Szöveges file-ban tárolt

Részletesebben

Programozás alapjai (ANSI C)

Programozás alapjai (ANSI C) Programozás alapjai (ANSI C) 1. Előadás vázlat A számítógép és programozása Dr. Baksáné dr. Varga Erika adjunktus Miskolci Egyetem, Informatikai Intézet Általános Informatikai Intézeti Tanszék www.iit.uni-miskolc.hu

Részletesebben

Kinek szól a könyv? A könyv témája A könyv felépítése Mire van szükség a könyv használatához? A könyvben használt jelölések. 1. Mi a programozás?

Kinek szól a könyv? A könyv témája A könyv felépítése Mire van szükség a könyv használatához? A könyvben használt jelölések. 1. Mi a programozás? Bevezetés Kinek szól a könyv? A könyv témája A könyv felépítése Mire van szükség a könyv használatához? A könyvben használt jelölések Forráskód Hibajegyzék p2p.wrox.com xiii xiii xiv xiv xvi xvii xviii

Részletesebben

7. Laboratóriumi gyakorlat: Vezérlési szerkezetek II.

7. Laboratóriumi gyakorlat: Vezérlési szerkezetek II. 7. Laboratóriumi gyakorlat: Vezérlési szerkezetek II. A gyakorlat célja: 1. A shell vezérlő szerkezetei használatának gyakorlása. A használt vezérlő szerkezetek: if/else/fi, for, while while, select, case,

Részletesebben

Algoritmizálás és adatmodellezés tanítása 1. előadás

Algoritmizálás és adatmodellezés tanítása 1. előadás Algoritmizálás és adatmodellezés tanítása 1. előadás Algoritmus-leíró eszközök Folyamatábra Irányított gráf, amely csomópontokból és őket összekötő élekből áll, egyetlen induló és befejező éle van, az

Részletesebben

Harmadik gyakorlat. Számrendszerek

Harmadik gyakorlat. Számrendszerek Harmadik gyakorlat Számrendszerek Ismétlés Tízes (decimális) számrendszer: 2 372 =3 2 +7 +2 alakiérték valódi érték = aé hé helyiérték helyiértékek a tízes szám hatványai, a számjegyek így,,2,,8,9 Kettes

Részletesebben

Bevezetés a programozásba I 3. gyakorlat. PLanG: Programozási tételek. Programozási tételek Algoritmusok

Bevezetés a programozásba I 3. gyakorlat. PLanG: Programozási tételek. Programozási tételek Algoritmusok Pázmány Péter Katolikus Egyetem Információs Technológiai Kar Bevezetés a programozásba I 3. gyakorlat PLanG: 2011.09.27. Giachetta Roberto groberto@inf.elte.hu http://people.inf.elte.hu/groberto Algoritmusok

Részletesebben

Felvételi vizsga mintatételsor Informatika írásbeli vizsga

Felvételi vizsga mintatételsor Informatika írásbeli vizsga BABEȘ BOLYAI TUDOMÁNYEGYETEM MATEMATIKA ÉS INFORMATIKA KAR A. tételsor (30 pont) Felvételi vizsga mintatételsor Informatika írásbeli vizsga 1. (5p) Egy x biten tárolt egész adattípus (x szigorúan pozitív

Részletesebben

5. előadás. Programozás-elmélet. Programozás-elmélet 5. előadás

5. előadás. Programozás-elmélet. Programozás-elmélet 5. előadás Elemi programok Definíció Az S A A program elemi, ha a A : S(a) { a, a, a, a,..., a, b b a}. A definíció alapján könnyen látható, hogy egy elemi program tényleg program. Speciális elemi programok a kövekezők:

Részletesebben

A C programozási nyelv I. Bevezetés

A C programozási nyelv I. Bevezetés A C programozási nyelv I. Bevezetés Miskolci Egyetem Általános Informatikai Tanszék A C programozási nyelv I. (bevezetés) CBEV1 / 1 A C nyelv története Dennis M. Ritchie AT&T Lab., 1972 rendszerprogramozás,

Részletesebben

ÁTVÁLTÁSOK SZÁMRENDSZEREK KÖZÖTT, SZÁMÁBRÁZOLÁS, BOOLE-ALGEBRA

ÁTVÁLTÁSOK SZÁMRENDSZEREK KÖZÖTT, SZÁMÁBRÁZOLÁS, BOOLE-ALGEBRA 1. Tízes (decimális) számrendszerből: a. Kettes (bináris) számrendszerbe: Vegyük a 2634 10 -es számot, és váltsuk át bináris (kettes) számrendszerbe! A legegyszerűbb módszer: írjuk fel a számot, és húzzunk

Részletesebben

Amit a törtekről tudni kell Minimum követelményszint

Amit a törtekről tudni kell Minimum követelményszint Amit a törtekről tudni kell Minimum követelményszint Fontos megjegyzés: A szabályoknak nem a pontos matematikai meghatározását adtuk. Helyettük a gyakorlatban használható, egyszerű megfogalmazásokat írtunk.

Részletesebben

Gregorics Tibor Modularizált programok C++ nyelvi elemei 1

Gregorics Tibor Modularizált programok C++ nyelvi elemei 1 Gregorics Tibor Modularizált programok C++ nyelvi elemei 1 Függvények és paraméterátadás A függvény egy olyan programblokk, melynek végrehajtását a program bármelyik olyan helyéről lehet kezdeményezni

Részletesebben

Gyakorló feladatok Gyakorló feladatok

Gyakorló feladatok Gyakorló feladatok Gyakorló feladatok előző foglalkozás összefoglalása, gyakorlató feladatok a feltételes elágazásra, a while ciklusra, és sokminden másra amit eddig tanultunk Változók elnevezése a változók nevét a programozó

Részletesebben

sallang avagy Fordítótervezés dióhéjban Sallai Gyula

sallang avagy Fordítótervezés dióhéjban Sallai Gyula sallang avagy Fordítótervezés dióhéjban Sallai Gyula Az előadás egy kis példaprogramon keresztül mutatja be fordítók belső lelki világát De mit is jelent, az hogy fordítóprogram? Mit csinál egy fordító?

Részletesebben

KOVÁCS BÉLA, MATEMATIKA I.

KOVÁCS BÉLA, MATEMATIKA I. KOVÁCS BÉLA, MATEmATIkA I. 1 I. HALmAZOk 1. JELÖLÉSEk A halmaz fogalmát tulajdonságait gyakran használjuk a matematikában. A halmazt nem definiáljuk, ezt alapfogalomnak tekintjük. Ez nem szokatlan, hiszen

Részletesebben

Programozás alapjai gyakorlat. 4. gyakorlat Konstansok, tömbök, stringek

Programozás alapjai gyakorlat. 4. gyakorlat Konstansok, tömbök, stringek Programozás alapjai gyakorlat 4. gyakorlat Konstansok, tömbök, stringek Házi ellenőrzés (f0069) Valósítsd meg a linuxos seq parancs egy egyszerűbb változatát, ami beolvas két egész számot, majd a kettő

Részletesebben

Előfeltétel: legalább elégséges jegy Diszkrét matematika II. (GEMAK122B) tárgyból

Előfeltétel: legalább elégséges jegy Diszkrét matematika II. (GEMAK122B) tárgyból ÜTEMTERV Programozás-elmélet c. tárgyhoz (GEMAK233B, GEMAK233-B) BSc gazdaságinformatikus, programtervező informatikus alapszakok számára Óraszám: heti 2+0, (aláírás+kollokvium, 3 kredit) 2019/20-es tanév

Részletesebben

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

A szemantikus elemzés helye. A szemantikus elemzés feladatai. A szemantikus elemzés feladatai. Deklarációk és láthatósági szabályok A szemantikus elemzés helye Forrásprogram Forrás-kezelő (source handler) Lexikális elemző (scanner) A szemantikus elemzés feladatai Fordítóprogramok előadás (A, C, T szakirány) Szintaktikus elemző (parser)

Részletesebben

Programozás Minta programterv a 2. házi feladathoz 1.

Programozás Minta programterv a 2. házi feladathoz 1. Programozás Minta programterv a. házi feladathoz 1. Gregorics Tibor. beadandó/0.feladat 01. január 11. EHACODE.ELTE gt@inf.elte.hu 0.csoport Feladat Egy szöveges állományban bekezdésekre tördelt szöveg

Részletesebben

OAF Gregorics Tibor : Memória használat C++ szemmel (munkafüzet) 1

OAF Gregorics Tibor : Memória használat C++ szemmel (munkafüzet) 1 OAF Gregorics Tibor : Memória használat C++ szemmel (munkafüzet) 1 Számábrázolás Számok bináris alakja A számítógépek memóriájában a számokat bináris alakban (kettes számrendszerben) ábrázoljuk. A bináris

Részletesebben

A programozás alapjai

A programozás alapjai A programozás alapjai Változók A számítógép az adatokat változókban tárolja A változókat alfanumerikus karakterlánc jelöli. A változóhoz tartozó adat tipikusan a számítógép memóriájában tárolódik, szekvenciálisan,

Részletesebben

Információk. Ismétlés II. Ismétlés. Ismétlés III. A PROGRAMOZÁS ALAPJAI 2. Készítette: Vénné Meskó Katalin. Algoritmus. Algoritmus ábrázolása

Információk. Ismétlés II. Ismétlés. Ismétlés III. A PROGRAMOZÁS ALAPJAI 2. Készítette: Vénné Meskó Katalin. Algoritmus. Algoritmus ábrázolása 1 Információk 2 A PROGRAMOZÁS ALAPJAI 2. Készítette: Vénné Meskó Katalin Elérhetőség mesko.katalin@tfk.kefo.hu Fogadóóra: szerda 9:50-10:35 Számonkérés időpontok Április 25. 9 00 Május 17. 9 00 Június

Részletesebben

Programozás I. Matematikai lehetőségek Műveletek tömbökkel Egyszerű programozási tételek & gyakorlás V 1.0 OE-NIK,

Programozás I. Matematikai lehetőségek Műveletek tömbökkel Egyszerű programozási tételek & gyakorlás V 1.0 OE-NIK, Programozás I. Matematikai lehetőségek Műveletek tömbökkel Egyszerű programozási tételek & gyakorlás OE-NIK, 2013 1 Hallgatói Tájékoztató A jelen bemutatóban található adatok, tudnivalók és információk

Részletesebben

INFORMATIKA tétel 2019

INFORMATIKA tétel 2019 INFORMATIKA tétel 2019 ELIGAZÍTÁS: 1 pont hivatalból; Az 1-4 feladatokban (a pszeudokód programrészletekben): (1) a kiír \n utasítás újsorba ugratja a képernyőn a kurzort; (2) a / operátor osztási hányadost

Részletesebben

Bánsághi Anna 2014 Bánsághi Anna 1 of 68

Bánsághi Anna 2014 Bánsághi Anna 1 of 68 IMPERATÍV PROGRAMOZÁS Bánsághi Anna anna.bansaghi@mamikon.net 3. ELŐADÁS - PROGRAMOZÁSI TÉTELEK 2014 Bánsághi Anna 1 of 68 TEMATIKA I. ALAPFOGALMAK, TUDOMÁNYTÖRTÉNET II. IMPERATÍV PROGRAMOZÁS Imperatív

Részletesebben

1.1. A forrásprogramok felépítése Nevek és kulcsszavak Alapvető típusok. C programozás 3

1.1. A forrásprogramok felépítése Nevek és kulcsszavak Alapvető típusok. C programozás 3 Darvay Zsolt Típusok és nevek a forráskódban Állandók és változók Hatókörök és az előfeldolgozó Bevitel és kivitel Kifejezések Utasítások Mutatók Függvények Struktúrák és típusok Állománykezelés C programozás

Részletesebben

2018, Funkcionális programozás

2018, Funkcionális programozás Funkcionális programozás 6. előadás Sapientia Egyetem, Matematika-Informatika Tanszék Marosvásárhely, Románia mgyongyi@ms.sapientia.ro 2018, tavaszi félév Miről volt szó? Haskell modulok, kompilálás a

Részletesebben

Amit a törtekről tudni kell 5. osztály végéig Minimum követelményszint

Amit a törtekről tudni kell 5. osztály végéig Minimum követelményszint Amit a törtekről tudni kell. osztály végéig Minimum követelményszint Fontos megjegyzés: A szabályoknak nem a pontos matematikai meghatározását adtuk. Helyettük a gyakorlatban használható, egyszerű megfogalmazásokat

Részletesebben

BASH script programozás II. Vezérlési szerkezetek

BASH script programozás II. Vezérlési szerkezetek 06 BASH script programozás II. Vezérlési szerkezetek Emlékeztető Jelölésbeli különbség van parancs végrehajtása és a parancs kimenetére való hivatkozás között PARANCS $(PARANCS) Jelölésbeli különbség van

Részletesebben

ALGORITMIKUS SZERKEZETEK ELÁGAZÁSOK, CIKLUSOK, FÜGGVÉNYEK

ALGORITMIKUS SZERKEZETEK ELÁGAZÁSOK, CIKLUSOK, FÜGGVÉNYEK ALGORITMIKUS SZERKEZETEK ELÁGAZÁSOK, CIKLUSOK, FÜGGVÉNYEK 1. ELÁGAZÁSOK ÉS CIKLUSOK SZERVEZÉSE Az adatszerkezetek mellett a programok másik alapvető fontosságú építőkövei az ún. algoritmikus szerkezetek.

Részletesebben

Segédanyagok. Formális nyelvek a gyakorlatban. Szintaktikai helyesség. Fordítóprogramok. Formális nyelvek, 1. gyakorlat

Segédanyagok. Formális nyelvek a gyakorlatban. Szintaktikai helyesség. Fordítóprogramok. Formális nyelvek, 1. gyakorlat Formális nyelvek a gyakorlatban Formális nyelvek, 1 gyakorlat Segédanyagok Célja: A programozási nyelvek szintaxisának leírására használatos eszközök, módszerek bemutatása Fogalmak: BNF, szabály, levezethető,

Részletesebben

Bevezetés a programozásba. 6. Előadás: C++ bevezető

Bevezetés a programozásba. 6. Előadás: C++ bevezető Bevezetés a programozásba 6. Előadás: C++ bevezető ISMÉTLÉS PLanG features Utasítások Értékadás, KI:, BE: Programkonstrukciók Elágazás Ciklus Típusok Egész, valós, logikai, szöveg, karakter, fájl Típuskonstrukciók

Részletesebben

Alkalmazott modul: Programozás 4. előadás. Procedurális programozás: iteratív és rekurzív alprogramok. Alprogramok. Alprogramok.

Alkalmazott modul: Programozás 4. előadás. Procedurális programozás: iteratív és rekurzív alprogramok. Alprogramok. Alprogramok. Eötvös Loránd Tudományegyetem Informatikai Kar Alkalmazott modul: Programozás 4. előadás Procedurális programozás: iteratív és rekurzív alprogramok Giachetta Roberto groberto@inf.elte.hu http://people.inf.elte.hu/groberto

Részletesebben

Bevezetés a programozásba I.

Bevezetés a programozásba I. Bevezetés a programozásba I. 9. gyakorlat Intelligens tömbök, mátrixok, függvények Surányi Márton PPKE-ITK 2010.11.09. C++-ban van lehetőség (statikus) tömbök használatára ezeknek a méretét fordítási időben

Részletesebben

Web-programozó Web-programozó

Web-programozó Web-programozó Az Országos Képzési Jegyzékről és az Országos Képzési Jegyzékbe történő felvétel és törlés eljárási rendjéről szóló 133/2010. (IV. 22.) Korm. rendelet alapján. Szakképesítés, szakképesítés-elágazás, rész-szakképesítés,

Részletesebben

Pénzügyi algoritmusok

Pénzügyi algoritmusok Pénzügyi algoritmusok A C++ programozás alapjai Az Integrált Fejlesztői Környezet C++ alapok Az Integrált Fejlesztői Környezet Visual Studio 2013 Community Edition Kitekintés: fordítás Preprocesszor Fordító

Részletesebben

INFORMATIKAI ALAPISMERETEK

INFORMATIKAI ALAPISMERETEK Informatikai alapismeretek középszint 0621 ÉRETTSÉGI VIZSGA 2007. május 25. INFORMATIKAI ALAPISMERETEK KÖZÉPSZINTŰ ÍRÁSBELI ÉRETTSÉGI VIZSGA JAVÍTÁSI-ÉRTÉKELÉSI ÚTMUTATÓ OKTATÁSI ÉS KULTURÁLIS MINISZTÉRIUM

Részletesebben

Bevezetés a programozásba I.

Bevezetés a programozásba I. Bevezetés a programozásba I. 5. gyakorlat Surányi Márton PPKE-ITK 2010.10.05. C++ A C++ egy magas szint programozási nyelv. A legels változatot Bjarne Stroutstrup dolgozta ki 1973 és 1985 között, a C nyelvb

Részletesebben

Algoritmizálás és adatmodellezés tanítása beadandó feladat: Algtan1 tanári beadandó /99 1

Algoritmizálás és adatmodellezés tanítása beadandó feladat: Algtan1 tanári beadandó /99 1 Algoritmizálás és adatmodellezés tanítása beadandó feladat: Algtan1 tanári beadandó /99 1 Készítette: Gipsz Jakab Neptun-azonosító: ABC123 E-mail: gipszjakab@seholse.hu Kurzuskód: IT-13AAT1EG Gyakorlatvezető

Részletesebben

Szerző Lővei Péter LOPSAAI.ELTE IP-08PAEG/25 Daiki Tennó

Szerző Lővei Péter LOPSAAI.ELTE IP-08PAEG/25 Daiki Tennó Szerző Név: Lővei Péter ETR-azonosító: LOPSAAI.ELTE Drótposta-cím: petyalovei@gmail.com Kurzuskód: IP-08PAEG/25 Gyakorlatvezető neve: Daiki Tennó Feladatsorszám: 11 1 Tartalom Szerző... 1 Tartalom... 2

Részletesebben