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

Hasonló dokumentumok
Programozási alapismeretek 1. előadás

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

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

Bevezetés az informatikába

Maximum kiválasztás tömbben

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

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

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.

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

Bevezetés a programozásba I.

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

1. Alapok. #!/bin/bash

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

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

Vezérlési szerkezetek

Webprogramozás szakkör

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

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

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

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

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

Programozás alapjai gyakorlat. 2. gyakorlat C alapok

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

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

Programozás C++ -ban 2007/1

Szerző. Varga Péter ETR azonosító: VAPQAAI.ELTE cím: Név: Kurzuskód:

A PiFast program használata. Nagy Lajos

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

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.

Szoftvertervezés és -fejlesztés I.

INFORMATIKA javítókulcs 2016

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

Regionális forduló november 18.

Java programozási nyelv

A KÓDOLÁS TECHNIKAI ELVEI

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

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

Bevezetés a programozásba

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

Felvételi tematika INFORMATIKA

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

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!

A C# programozási nyelv alapjai

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

5. Gyakorlat. struct diak {

1. Alapok. Programozás II

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

A programozás alapjai 1 Rekurzió

Programozás C és C++ -ban

Programozási segédlet

Rekurzió. Dr. Iványi Péter

Aritmetikai kifejezések lengyelformára hozása

Már megismert fogalmak áttekintése

C programozási nyelv

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

A C programozási nyelv I. Bevezetés

Bevezetés a C++ programozási nyelvbe

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

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

Programozás I. gyakorlat

Programozás alapjai (ANSI C)

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?

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

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

Harmadik gyakorlat. Számrendszerek

Bevezetés a programozásba I 3. gyakorlat. PLanG: Programozási tételek. Programozási tételek Algoritmusok

Felvételi vizsga mintatételsor Informatika írásbeli vizsga

5. előadás. Programozás-elmélet. Programozás-elmélet 5. előadás

A C programozási nyelv I. Bevezetés

ÁTVÁLTÁSOK SZÁMRENDSZEREK KÖZÖTT, SZÁMÁBRÁZOLÁS, BOOLE-ALGEBRA

Amit a törtekről tudni kell Minimum követelményszint

Gregorics Tibor Modularizált programok C++ nyelvi elemei 1

Gyakorló feladatok Gyakorló feladatok

sallang avagy Fordítótervezés dióhéjban Sallai Gyula

KOVÁCS BÉLA, MATEMATIKA I.

Programozás alapjai gyakorlat. 4. gyakorlat Konstansok, tömbök, stringek

Előfeltétel: legalább elégséges jegy Diszkrét matematika II. (GEMAK122B) tárgyból

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

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

OAF Gregorics Tibor : Memória használat C++ szemmel (munkafüzet) 1

A programozás alapjai

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

Programozás I. Matematikai lehetőségek Műveletek tömbökkel Egyszerű programozási tételek & gyakorlás V 1.0 OE-NIK,

INFORMATIKA tétel 2019

Bánsághi Anna 2014 Bánsághi Anna 1 of 68

1.1. A forrásprogramok felépítése Nevek és kulcsszavak Alapvető típusok. C programozás 3

2018, Funkcionális programozás

Amit a törtekről tudni kell 5. osztály végéig Minimum követelményszint

BASH script programozás II. Vezérlési szerkezetek

ALGORITMIKUS SZERKEZETEK ELÁGAZÁSOK, CIKLUSOK, FÜGGVÉNYEK

Segédanyagok. Formális nyelvek a gyakorlatban. Szintaktikai helyesség. Fordítóprogramok. Formális nyelvek, 1. gyakorlat

Bevezetés a programozásba. 6. Előadás: C++ bevezető

Alkalmazott modul: Programozás 4. előadás. Procedurális programozás: iteratív és rekurzív alprogramok. Alprogramok. Alprogramok.

Bevezetés a programozásba I.

Web-programozó Web-programozó

Pénzügyi algoritmusok

INFORMATIKAI ALAPISMERETEK

Bevezetés a programozásba I.

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

Szerző Lővei Péter LOPSAAI.ELTE IP-08PAEG/25 Daiki Tennó

Átírás:

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

Egyetemi jegyzet 2012 2

ELŐSZÓ... 8 BEVEZETÉS... 10 I. RÉSZ ALAPOK... 17 1. ELSŐ LÉPÉSEK... 20 Implementációs stratégia... 20 Nyelvi elemek... 23 1. Feladat: Osztási maradék... 28 2. Feladat: Változó csere... 39 C++ kislexikon... 44 2. STRUKTURÁLT PROGRAMOK... 45 Implementációs stratégia... 45 Nyelvi elemek... 46 3. Feladat: Másodfokú egyenlet... 61 4. Feladat: Legnagyobb közös osztó... 73 5. Feladat: Legnagyobb közös osztó még egyszer... 80 C++ kislexikon... 86 3. TÖMBÖK... 88 Implementációs stratégia... 88 Nyelvi elemek... 90 6. Feladat: Tömb maximális eleme... 96 7. Feladat: Mátrix maximális eleme... 111 8. Feladat: Melyik szóra gondoltam... 117 C++ kislexikon... 127 4. KONZOLOS BE- ÉS KIMENET... 129 Implementációs stratégia... 129 Nyelvi elemek... 132 3

9. Feladat: Duna vízállása... 142 10. Feladat: Alsóháromszög-mátrix... 150 C++ kislexikon... 163 5. SZÖVEGES ÁLLOMÁNYOK... 167 Implementációs stratégia... 167 Nyelvi elemek... 170 11. Feladat: Szöveges állomány maximális eleme... 179 12. Feladat: Jó tanulók kiválogatása... 187 C++ kislexikon... 194 II. RÉSZ PROCEDURÁLIS PROGRAMOZÁS... 199 6. ALPROGRAMOK A KÓDBAN... 202 Implementációs stratégia... 202 Nyelvi elemek... 205 13. Feladat: Faktoriális... 211 14. Feladat: Adott számmal osztható számok... 218 15. Feladat: Páros számok darabszáma... 230 C++ kislexikon... 236 7. PROGRAMOZÁSI TÉTELEK IMPLEMENTÁLÁSA... 239 Implementációs stratégia... 239 Nyelvi elemek... 242 16. Feladat: Legnagyobb osztó... 247 17. Feladat: Legkisebb adott tulajdonságú elem... 262 18. Feladat: Keressünk Ibolyát... 272 C++ kislexikon... 282 8. TÖBBSZÖRÖS VISSZAVEZETÉS ALPROGRAMOKKAL... 285 Implementációs stratégia... 286 4

Nyelvi elemek... 290 19. Feladat: Kitűnő tanuló... 292 20. Feladat: Azonos színű oldalak... 304 21. Feladat: Mátrix párhozamos átlói... 315 C++ kislexikon... 330 9. FORDÍTÁSI EGYSÉGEKRE BONTOTT PROGRAM... 331 Implementációs stratégia... 332 Nyelvi elemek... 334 22. Feladat: Műkorcsolya verseny... 337 23. Feladat: Melyikből hány van... 365 C++ kislexikon... 374 10. REKURZÍV PROGRAMOK KÓDOLÁSA... 376 Implementációs stratégia... 376 Nyelvi elemek... 378 24. Feladat: Binomiális együttható... 380 25. Feladat: Hanoi tornyai... 390 26. Feladat: Quick sort... 397 III. RÉSZ PROGRAMOZÁS OSZTÁLYOKKAL... 409 11. A TÍPUS MEGVALÓSÍTÁS ESZKÖZE: AZ OSZTÁLY... 411 Implementációs stratégia... 411 Nyelvi háttér... 414 27. Feladat: UFO-k... 423 28. Feladat: Zsák... 439 29. Feladat: Síkvektorok... 450 C++ kislexikon... 465 12. FELSOROLÓK TÍPUSAINAK MEGVALÓSÍTÁSA... 467 5

Implementációs stratégia... 467 Nyelvi háttér... 470 30. Feladat: Könyvtár... 476 31. Feladat: Havi átlag-hőmérséklet... 498 32. Feladat: Bekezdések... 520 C++ kislexikon... 539 13. DINAMIKUS SZERKEZETŰ TÍPUSOK OSZTÁLYAI... 541 Implementációs stratégia... 542 Nyelvi elemek... 545 33. Feladat: Verem... 556 34. Feladat: Kettős sor... 586 C++ kislexikon... 610 14. OBJEKTUM-ORIENTÁLT KÓD-ÚJRAFELHASZNÁLÁSI TECHNIKÁK... 612 Implementációs stratégia... 613 Nyelvi elemek... 618 35. Feladat: Túlélési verseny... 622 36. Feladat: Lengyel forma és kiértékelése... 642 37. Feladat: Bináris fa bejárása... 677 C++ kislexikon... 705 15. EGY OSZTÁLY-SABLON KÖNYVTÁR FELHASZNÁLÁSA... 707 Osztály-sablon könyvtár tervezése... 708 Osztály-sablon könyvtár implementálása... 717 38. Feladat: Kiválogatás... 729 39. Feladat: Feltételes maximumkeresés... 734 40. Feladat: Keresés... 739 41. Feladat: Leghosszabb szó W betűvel... 751 6

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

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

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 4.2.1./B-09/1/KMR-2010-0003). 9

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

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

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

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

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

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

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

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

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

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

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

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

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ó. 1-2. á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

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

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

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) = 10110111 (2) = 1*2 7 + 0*2 6 + 1*2 5 + 1*2 4 + 0*2 3 + 1*2 2 + 1*2 1 + 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 0.8125 (10) bináris alakját! 0.8125 * 2 = 1.6250 egészrész: 1 törtrész: 0.625 0.625 * 2 = 1.250 egészrész: 1 törtrész: 0.25 0.25 * 2 = 0.50 egészrész: 0 törtrész: 0.5 0.5 * 2 = 1.0 egészrész: 1 törtrész: 0 Tehát 0.8125 (10) = 0.1101 (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: 0.2 0.2 * 2 = 0.4 egészrész: 0 törtrész: 0.4 0.4 * 2 = 0.8 egészrész: 0 törtrész: 0.8 0.8 * 2 = 1.6 egészrész: 1 törtrész: 0.6 0.6 * 2 = 1.2 egészrész: 1 törtrész: 0.2 0.2 * 2 = 0.4 egészrész: 0 törtrész: 0.4 0.4 * 2 = 0.8 egészrész: 0 törtrész: 0.8 Tehát 0.1 (10) = 0.00011 (2) (végtelen szakaszos tört) 25

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

<< 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

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

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

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

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

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

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) = 00000000 00000000 00000000 00001100 (2) 2. Példa. Adjuk meg a -12 egész szám kettes komplemens kódját! i) 12 (10) = 00000000 00000000 00000000 00001100 (2) ii) Vegyük a bináris alak komplemensét (invertált alakját)! 11111111 11111111 11111111 11110011 iii) Adjunk hozzá binárisan 1-et! 11111111 11111111 11111111 11110100 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

1.23.23 0.23 1 1.0 1.2e10 1.23e-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 100 00 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 01111111 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 -12.25 valós szám lebegőpontos kódját 1+11+52 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: 12.25 (10) = 1100.01 (2) Normalizáljuk az így kapott számot 1 és 2 közé: 1100.01 (2) = 1.10001 (2) *2 3 Ábrázoljuk a karakterisztikát többletes kódban 11 biten: 3 + 2 10 1 (10) = 10000000010 (2) A kód: 1 10000000010 10001000000000000000 00000000 51

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

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

<=, >, >=). 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

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

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

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

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

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. 2-5. á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

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

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

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

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

#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

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

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

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

(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 4. (0.0,1.0,-5.2) Válasz: elsőfokú gyöke: -5.2 5. (0.0,3.0,-5.2) Válasz: elsőfokú gyöke: -1.733 6. (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

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 11. (1.0,-5.0,6.0) Válasz: két valós gyök: 2.0 és 3.0 12. (1.0,5.0,6.0) Válasz: két valós gyök: -2.0 és -3.0 13. (1.0,-1.0,6.0) Válasz: két valós gyök: 3.0 és -2.0 14. (1.0,1.0,-6.0) Válasz: két valós gyök: -3.0 és 2.0 15. (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

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

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

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

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

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

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

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

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

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

//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

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

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

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

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

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

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

C++ kislexikon Típus Jele Értékei Műveletei Megjegyzés Természetes Egész int 83, -1320 + - * / % ==!= < <= > >= egész osztás Valós double -27.72, 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

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

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

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

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. 3-2. á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

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

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

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

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

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

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

(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

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

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

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

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

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

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

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

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

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

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

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

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

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

#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

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

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

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

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

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 +1 117

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

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

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

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

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

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

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

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

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

// 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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) 3. 1 1-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) 4. 2 2-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) 5. 3 3-es mátrixok szorzása (lásd 2 2-es eseteket) 6. 5 5-es mátrixok szorzása (lásd 2 2-es eseteket) 7. A kommutativitás vizsgálata Érvénytelen tesztesetek: 155

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

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

// 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

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

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

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

<< 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

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

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

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

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

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

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

á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. 5-1. á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

é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

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

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

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

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

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

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

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 1987 4.8"; 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

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

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

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

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

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

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

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

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

// 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

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 1 2 3 3 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

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

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

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

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

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

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

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

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

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

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

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

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

ü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

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

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. 6-1. ábra. Alprogram kialakításának okai 202

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

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

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

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

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

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

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

é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

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

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

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

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

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

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

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

{ 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

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

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

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

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

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

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

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

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

{ 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

{ 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

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

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

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

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

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

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

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

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

return 5*k; 238

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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ó 4. 2. 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ó 1. 3. 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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) {... 279

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

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

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

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

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

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

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

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. 8-1. á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

é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

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

á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

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

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

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

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

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

{ 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

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

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

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

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

// 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

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

"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

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

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

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

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

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

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

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

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

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

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

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

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 + 1 315

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

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

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

{ 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

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

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

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

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

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

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

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

{ 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

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

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

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

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

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. 9-1. á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

(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. 9-2. á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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

{ 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

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

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

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

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

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

<< 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

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

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

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

#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

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

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

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

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

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

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

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

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

; 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

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

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

return 0; 372

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

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

á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

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

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

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

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

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

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

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 10-1. á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

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

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

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

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

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

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

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

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

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() 10-3. á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() 10-4. á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

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

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

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

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

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

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

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

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() 10-5. á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() 10-6. ábra. Alprogramok hívási láncai A fő program 399

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

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

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

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

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

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

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

#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

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

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

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

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

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. 11-1. á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

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 11-2. á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

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

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

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

(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 11-2. á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

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

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

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

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

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

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 10000 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 2 2 2 d : ( p. x q. x) ( p. y q. y) ( p. z q. z) Absztrakt program 423

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() 11-3. ábra. Komponens szerkezet 424

425

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() 11-4. á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

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

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

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

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

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

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

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

"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

#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

#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

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

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

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

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() 11-5. á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

Read() main() Bag() Put() Max() 11-6. á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

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

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

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

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

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

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

#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

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

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

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

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() 11-7. á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+=() 11-8. ábra. Alprogramok hívási lánca 452

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

{ 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

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

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

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

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

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

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

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

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

#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

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

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

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

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

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 12-3. á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

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 12-3. á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

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

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

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

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

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

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);... 475

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

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() 12-5. ábra. Alprogramok hívási láncai 477

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

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

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

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() 12-6. á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() 12-7. ábra. Alprogramok hívási láncai 481

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

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

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

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

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

{ 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

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

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

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

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

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

#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

#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

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

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

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

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

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

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

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

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() 12-8. á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

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() 12-9. ábra. Alprogramok hívási láncai Pair_Enor osztály 503

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

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() 12-10. á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() 12-11. ábra. Alprogramok hívási láncai 505

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

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

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

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

{ 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

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

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

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

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

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

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

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

else st = abnorm; 519

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

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

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

(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

main.cpp enor.h-enor.cpp main() class Enor Enor() First() Next() End() Current() ~Enor() 12-12. á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

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

Enor() First() main() Next() End() Current Current() 12-13. á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

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

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

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

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

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

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

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

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

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

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

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

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

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

... 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);... 540

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

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

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

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 13-1. á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

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 13-2. á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

(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 13-3. á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

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

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 0 1 2 3 13-4. á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

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 0 1 2 3 cím 10 11 12 13 20 21 22 23 13-5. á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

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

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

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

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 13-6. ábra. Egyirányú lista felsoroló műveletei 553

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

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

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

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

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

Implementálás A program komponens szerkezete main.cpp stack.h-stack.cpp main() class Stack Stack() ~Stack() Push() Pop() Top() Empty() 13-7. á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){... 559

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

#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

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

{ 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

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

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

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

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

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

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

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

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

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

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() 13-8. á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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

; 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

#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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

é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

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

T szivacs 10 0 2 1 0 2 0 1 0 1 2 623

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. 14-1. ábra. Lények osztálydiagrammja 624

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 +6-625

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

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

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

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

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

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

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

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

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

É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

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

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

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

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

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

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

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

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. 14-2. ábra. Tokenek osztálydiagrammja 643

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

>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

é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

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

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

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

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

<< 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

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

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

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

// 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

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

// 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

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

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

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

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 #endif

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

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

#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

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

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

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

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

#endif 670

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

: 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

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

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

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

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

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

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

Item Item Item Item 14-3. á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

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

é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 14-4. ábra. Tevékenységek osztálydiagrammja Implementálás A program komponens szerkezete 681

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

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

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

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

{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

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

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

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

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

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

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

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

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

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

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

; 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

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

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

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

; 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

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

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

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

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 Fv<int>( )

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

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. 15-1. ábra. Az felsorolók absztrakt osztály-sablon 708

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). 15-2. á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

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

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

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. 15-5. á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

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. 15-6. á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

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

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

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

Item Item Item Item Item,ResultType Item,Value,Compare Item Item,optimist Item Item Item 15-10. á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