programozÁsez a könyv annak a programozás című sorozatnak a második kötete, amelyet az...

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

Upload: others

Post on 02-Feb-2020

8 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: PROGRAMOZÁSEz 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

1

Gregorics Tibor

PROGRAMOZÁS

2. kötet

MEGVALÓSÍTÁS

Page 2: PROGRAMOZÁSEz 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

2

Egyetemi jegyzet

2012

Page 3: PROGRAMOZÁSEz 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

3

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

Page 4: PROGRAMOZÁSEz 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

4

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

Page 5: PROGRAMOZÁSEz 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

5

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

Page 6: PROGRAMOZÁSEz 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

6

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

Page 7: PROGRAMOZÁSEz 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

7

42. Feladat: Összefuttatás ........................................................... 767

IRODALOM JEGYZÉK ............................................................................................ 781

Page 8: PROGRAMOZÁSEz 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

8

ELŐSZÓ

Ez a könyv annak a Programozás című sorozatnak a második kötete,

amelyet az Eötvös Loránd Tudományegyetem programtervező informatikus

szakának azon tantárgyaihoz ajánlok, amelyeken a hallgatók az első

benyomásaikat szerezhetik meg a programozás szakmájáról. Az első kötet a

Tervezés alcímet viseli, és a programozási feladatokat megoldó algoritmusok

előállításával foglalkozik. Ennek a kötetnek pedig a Megvalósítás az alcíme,

mert itt az előző kötet alapján elkészített tervek kivitelezéséről lesz szó,

amely során a tervet egy konkrét programozási környezetben

implementáljuk, programkódot hozunk létre és futtatható alkalmazást

készítünk.

Ebben a kötetben is a tervezéssel kezdem egy feladat megoldását, de

azt nem kommentálom, hiszen erről az első kötetben részletesen esik szó.

Inkább azokra a kérdésekre szeretnék válaszolni, amelyek az implementáció

során vetődnek fel. Olyan döntési szituációkat, stratégiákat veszek sorra,

amelyekkel a programozó a terv megvalósítása során találkozik. Mivel pedig

a végcél egy szoftver előállítása, ezért kellő figyelmet fordítok majd a

tesztelésre, sőt, a megoldások leírásával mintát adok a kielégítő

dokumentálásra is.

Egy olyan könyvben, amelyik működő alkalmazások készítéséről szól,

megkerülhetetlen annak a programozási nyelvnek a bemutatása, amellyel

programjainkat kódoljuk. Nincs szándékom azonban programozási

nyelvkönyvet írni, hiszen ezekből sok jót találni a könyvesboltokban. Az

implementáció folyamata, döntési szituációi, stratégiái különben sem

kötődnek egy-egy konkrét programozási nyelvhez. Természetesen legalább

egy konkrét nyelven meg kell valósítani a megoldásokat, és ez a nyelv ebben

a kötetben a C++ lesz, de szeretném, ha a kötet tanácsait más nyelveken

történő megvalósítás esetén is fel lehetne használni.

Ennek a kötetnek az első része a programtervező informatikus szak

Programozási alapismeretek tárgyához nyújt közvetlen segítséget, második

része és a harmadik rész első két fejezete a Programozás tantárgy

implementációról szóló ismeretanyagát fogja át, utolsó három fejezete pedig

Page 9: PROGRAMOZÁSEz 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

9

az Objektumelvű alkalmazások fejlesztése tantárgyhoz kapcsolódik. A

kötetben szereplő mintapéldák egy része a képzésünkben már megszűnt

Alkalmazások fejlesztése I. és II. című tantárgyból származik. Ennek kapcsán

feltétlenül meg kell említenem Szabóné Nacsa Rozália, Sike Sándor, Steingart

Ferenc és Porkoláb Zoltán nevét, akik az említett elődtantárgynak a

kidolgozásában részt vettek, és közvetve hozzájárultak ennek a kötetnek a

megszületéséhez.

Ez a kötet – akárcsak az első – gyakorlatorientált. Negyvenkét feladat

megoldásának részletes bemutatása található benne, amely kiegészül a

szükséges ismeretek (implementációs stratégiák, nyelvi eszközök) leírásával.

A kötetben külön gyakorló feladatok nincsenek, gyakorlásként az első, a

tervezésről szóló kötet feladatainak megoldási tervét lehet megvalósítani.

A két kötetet (ezt a megvalósításról szólót illetve a tervezésről szóló

első kötetet) úgy terveztem, hogy ezek egymástól függetlenül is érthetőek

legyenek, de az ajánlott olvasási sorrend az, hogy a kötetek egyes részeit

párban dolgozzák fel a hallgatók. Az első kötet első részében az alapvető

programozási fogalmakat vezetem be. Ennek megismerése után ennek a

kötetnek az első részét érdemes áttekinteni, amely nagyon egyszerű

programok készítését, és azokhoz szükséges nyelvi elemeket mutatja be. Az

első kötet második részében a visszavezetésre épülő programtervezési

technikát találjuk, míg itt a második rész az ilyen módon előállított

programtervek megvalósításáról szól. A harmadik rész mindkét kötetben a

korszerű típus fogalmához kapcsolódik. Az első kötetben a típus-központú

tervezésről, ebben a kötetben a felhasználói típusok osztályokkal történő

megvalósításáról olvashatunk, azaz az objektum-orientált programozás

alapjaival ismerkedhetünk meg.

A jegyzet tananyagának kialakítása az Európai Unió támogatásával, az

Európai Szociális Alap társfinanszírozásával valósult meg (a támogatás száma

TÁMOP 4.2.1./B-09/1/KMR-2010-0003).

Page 10: PROGRAMOZÁSEz 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

10

BEVEZETÉS

A programkészítés egy olyan folyamat, amely magába foglalja a feladat

elemzését, a megoldás tervezését, a programkód előállítását, majd

tesztelését és végül a dokumentálást. Ezek sorrendjét mutatja be a szoftver

előállításának hagyományos, úgynevezett vízesés modellje (1.ábra). A

valóságban egy program elkészítése nem ennyire szabályos mederben folyik.

Minél összetettebb ugyanis a megoldandó probléma, annál többször fordul

elő, hogy a megoldás előállításához vezető út egyik vagy másik korábban

elvégzett szakaszához vissza kell térni, azt újra kell gondolni, és ennek

következtében az azt követő szakaszokat (még ha nem is teljes egészében)

meg kell ismételni. A vízesés modell nem azt mutatja meg, hogy hány

lépésben áll elő a megoldás, hanem csak azt, hogy milyen természetű

kérdésekkel kell foglalkozni a programkészítés során, és az egyes

szakaszokban hozott döntések mely más szakaszokra vannak hatással.

1.ábra Egyszerűsített vízesés modell

Tesztelés, Dokumentálás

Megvalósítás

Elemzés

Tervezés

Feladat

Specifikáció

Programterv

Programkód

Megoldás

Page 11: PROGRAMOZÁSEz 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

11

A modell például egyértelműen jelzi, hogy egy probléma megoldásánál

először a probléma elemzésével kell foglalkozni (mi a feladat, milyen

adatokat ismerünk, mit kell kiszámolnunk, azaz mi a cél, milyen formában

állítsuk elő a választ, milyen eszközök állnak rendelkezésünkre a

megvalósításhoz, stb.). Ezt követően lehet a megoldó program logikáját,

absztrakt vázát megtervezni. Ha tervezés során kiderül, hogy a feladat nem

teljesen világos, akkor vissza kell térni a feladat elemzéséhez. Csak a terv

(még ha ez nem is végleges) birtokában kezdhetjük el a program adott

programozási nyelven történő implementálását, majd az így kapott

programkód ismételt futtatásaival végezhető el a megoldás módszeres

kipróbálása, a tesztelés. Nem megfelelő teszteredmények esetén a megelőző

lépések valamelyikét kell korrigálni, amely több más lépés javítását is

kiválthatja. Sohasem szabad azonban szem elől téveszteni azt, hogy az adott

pillanatban éppen melyik szakaszával foglalkozunk a megoldásnak. Nem

vezetne célhoz ugyanis, ha például a téves tervezés miatt rosszul működő

programnak a kódját módosítanánk a helyett, hogy a tervet vizsgálnánk felül.

Végül nem szabad megfeledkeznünk a megoldás dokumentálásáról sem,

hiszen egy programkód akkor válik termékké, ha annak használatát,

karbantartását, továbbfejlesztését mások számára is érthető módon leírjuk.

Sokszor hallani azt az érvet, hogy „egy egyszerű feladat megoldásánál

nincs szükség a tervezésre”. Ez nem igaz! Az lehet, hogy egy egyszerű

feladatot megoldó program esetében elég, ha a megoldás terve csak a

programozó fejében ölt testet. De terv ekkor is van, és a programozás

tanulása során eleinte ezt az egyszerű tervet sem árt írásban rögzíteni. A terv

magába foglalja a feladat elemzésének eredményét, a feladat adatait, az

adatok típusát, változóik nevét, azt, hogy ezek között melyek a bemenő

illetve eredményt hordozó változók, a bemenő változók milyen

előfeltételeknek tesznek eleget, egy szóval a feladat specifikációját. Az

eredményváltozókra megfogalmazott úgynevezett utófeltétel is a

specifikáció része, de erre az implementálásnál közvetlenül már nincs

szükség (az utófeltétel az absztrakt megoldás előállítását, illetve a program

tesztelését segíti), hanem helyette a megoldó program absztrakt vázát –

például struktogrammját – kell ismernünk. A terv tehát tartalmazza a

megoldás absztrakt, a konkrét programozási környezettől (számítógéptől,

Page 12: PROGRAMOZÁSEz 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

12

operációs rendszertől, programozási nyelvtől, fejlesztő környezettől)

elvonatkoztatott változatát.

A megoldás tervéből a megvalósítás (implementálás) során készül el a

működő program. A megvalósításnak az eszköze az a programozási nyelv és

programozási környezet (számítógép, operációs rendszer, fejlesztő

eszközök), amelyben a programot fejlesztjük. Az implementálás hangsúlyos

része a kódolás, amikor az absztrakt program utasításait a konkrét

programnyelv utasításaira írjuk át, és ehhez nélkülözhetetlen a konkrét

programozási nyelv ismerete. A programkód elkészítése azonban nem

pusztán a tervezés során előállt absztrakt program mechanikus lefordításából

(kódolásából) áll. Az implementálás számos olyan döntést és tevékenységet

is magába foglal, amely a programozási nyelvtől független. Például konzolos

alkalmazásoknál, amelyek kódolásával ebben a kötetben foglalkozunk, a terv

általában nem tér ki arra, hogy a bemenő változók hogyan vegyék fel a

kezdőértékeiket, illetve az eredményváltozók értékeit hogyan tudjuk a

felhasználó felé láthatóvá tenni. Pedig ezekről a kérdésekről, az adatok

beolvasásáról és az eredmény kiírásáról a programnak gondoskodnia kell. A

program előállításakor a feladat megoldási tervén túlmutató számottevő

kódot kell tehát előállítanunk, ezért nevezzük ezt a folyamatot a terv

kódolása helyett a terv implementálásának. Ugyancsak túlmutat a

mechanikus kódoláson az, amikor figyelembe kell venni az olyan nem

funkcionális követelményeket, mint például a memóriaigényre vagy futási

időre szabott feltételek. A hatékonyságra ugyan már a tervezési fázisban

lehet és kell figyelni, de a kódoláskor a megfelelő nyelvi eszközök

kiválasztásával is jelentősen lehet javítani a program memóriaigényét és

futási idejét. De ne felejtsük el, hogy egy rosszul működő program

hatékonyságával értelmetlen dolog foglalkozni; a hatékonyság kérdése csak

akkor kerül előtérbe, ha a program már képes megoldani a feladatot.

Ebben a könyvben programozási nyelvnek a C++ nyelvet választottam,

hiszen azoknál a tantárgyaknál, amelyeknek jegyzetéül szolgál ez a kötet,

ugyancsak a C++ nyelvet használjuk. A fejezetekben látszólag sok C++ nyelvi

elemet mutatok be (eleinte sok apró elemet, később egyre kevesebbet, bár

súlyát tekintve nagyobb horderejűt), mégsem e nyelvi elemek bemutatásán

van a hangsúly. A C++ nyelv csak illusztrálásul szolgál az implementáláshoz,

ezért nem is törekszem a teljes megismertetésére. Nem foglalkozok annak

Page 13: PROGRAMOZÁSEz 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

13

megmutatásával sem, hogy pontosan mi történik egy C++ utasítás

végrehajtáskor a számítógépen, ha annak megértését és felhasználhatóságát

valamilyen egyszerűbb modell segítségével is érthetővé lehet tenni. Első

sorban azokra a kérdésekre koncentrálok, amelyek az implementáció során

vetődnek fel.

Egy programozási nyelvnek az alapszintű megismeréséhez – ha ez nem

az első programozási nyelv, amivel találkozunk – néhány óra is elegendő. Ez

azért van így, mert egy gyakorlott programozó már tudja, hogy melyek egy

nyelv azon elemei, amelyeket mindenképpen meg kell ismerni ahhoz, hogy

egy program kódolását elkezdhessük. Megpróbálom a C++ nyelvet ilyen

szemmel bemutatni: mindig csak egy kis részt vizsgálok meg belőle, éppen

annyit, amelyet feltétlenül ismerni kell, hogy a soron következő programot el

lehessen készíteni. Nem vérbeli C++ programozók képzése a célom, hanem

olyanoké, akik meg tudnak oldani egy programozási feladatot „jobb híján”

C++ nyelven. De vajon lehetséges egyáltalán kódolásról beszélni a

programozási nyelv alapos ismerete nélkül. A válasz: igen. Ahhoz tudnám

hasonlítani ezt a helyzetet, mint amikor valakinek egy idegen nyelvet kell

megtanulnia. Ezt lehet az adott nyelv nyelvtanának tanulmányozásával

kezdeni (különösen, ha van már ismeretünk egy másik, mondjuk az

anyanyelvünk nyelvtanáról), vagy egyszerűen csak megpróbáljuk használni a

nyelvet (anélkül tanulunk meg egy kifejezést, hogy pontosan tudnánk, az

hány szóból áll, melyik az alany, melyik az állítmány). Ebben a kötetben ezt a

második utat követem, a feladat megoldását helyezem középpontba, és a

C++ nyelvről csak ott és éppen annyit mondok el, amire feltétlenül

szükségünk lesz. Értelemszerűen nem foglalkozok a hatékonyság

növelésének nyelvi sajátosságaival, hiszen ez a nyelv haladó szintű ismeretét

igényli, de igyekeztem minden helyzetre a legalkalmasabb nyelvi eszközt

megtalálni: olyat, amit a szakma támogat, amelynek a használata biztonságos

és hatékony, a leírása egyszerű, működésének magyarázata nem igényel

mély operációs rendszerbeli ismereteket, és kellően általános ahhoz, hogy

hasonló nyelvi elemet más nyelvekben is találjunk.

A mondanivalóm szempontjából a fejlesztő eszköz megválasztása még

annyira sem lényeges, mint a programozási nyelvé. Célszerű a kezdő

programozóknak úgynevezett integrált fejlesztő környezetet használniuk,

amely lehetőséget ad minden olyan tevékenység végzésére, amely a

Page 14: PROGRAMOZÁSEz 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

14

programozásnál szükséges. Támogatja a kód (C++ nyelven történő)

szerkesztését, a kódnak a számítógép nyelvére történő lefordítását (itt az

adott nyelv szabványos fordítóprogramját célszerű használni), a fordításnál

esetlegesen jelzett alaki (szintaktikai) hibák helyének könnyű megtalálását, a

futtatható gépi kódú program összeszerkesztését, a program futtatását,

tesztelését, valamint a tartalmi (szemantikai) hibák nyomkövetéssel történő

keresését.

Alkalmazásainkat minden esetben egy úgynevezett projektbe ágyazzuk

be. Eleinte, amíg az alkalmazásunk egyetlen forrás állományból áll, talán

körülményeskedőnek hat ez a lépés, de ha megszokjuk, akkor később, a több

állományra tördelt alkalmazások készítése is egyszerű lesz.

A programok kódolásánál törekedjünk arra, hogy a beírt kódot olyan

hamar ellenőrizzük fordítás és futtatás segítségével, amilyen hamar csak

lehet. Ne a teljes kód beírása után fordítsuk le először a programot, mert a

hibaüzenetek tényleges okát ekkor már sokkal nehezebb megtalálni! Eleinte

ne szégyelljük, ha utasításonként fordítunk, de később se írjunk le egy

program-blokknál többet fordítás nélkül. A bizonyítottan helyes absztrakt

program kódolása többnyire nem eredményez működő programot. A kódolás

során ugyanis elkövethetünk alaki (szintaktikai) hibákat (ezt ellenőrzi

fordításkor a fordító program) és tartalmi (szemantikai) hibákat. Ez utóbbiak

egy részét a kódolási megállapodások betartásával tudjuk kivédeni, más

részét teszteléssel felfedezni, a hiba okát pedig nyomkövetéssel megtalálni.

A fordításnál keletkezett hibaüzenetek értelmezése nem könnyű. A

fordítóprogram annál a sornál jelez hibát, ahol a hibát észlelte, de a hiba

gyakran több sorral előbb található vagy éppen nem található (ilyen például

egy változó deklarálásának hiánya). Érdemes a hibaüzeneteket sorban

értelmezni és kijavítani, mert bizonyos hibák egy korábbi hibának a

következményei. Fontos tudni, hogy a hibaüzenet nem adja meg a hiba

javításának módját. Nem szabad a hibaüzenet által sugallt javítást azonnal

végrehajtani, hanem először rá kell jönnünk a hiba valódi okára, majd meg

kell keresnünk, hol és mi módon korrigálhatjuk azt. Ne csak a fordító

hibaüzeneteit (error), hanem a figyelmeztetéseit (warning) is olvassuk el. A

figyelmeztetések rámutathatnak más, nehezen értelmezhető hibaüzenetek

Page 15: PROGRAMOZÁSEz 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

15

okaira, vagy később fellépő rejtett hibákra. A szerkesztési hibák egy része

elkerülhető a kódolási megállapodások betartásával.

Érdemes a kódot úgy elkészíteni, hogy annak bizonyos részei minél

hamarabb futtathatók legyenek. Így a programot már akkor ki tudjuk

próbálni, amikor az még nem a teljes feladatot oldja meg.

Ne sajnáljuk a tesztelésre szánt időt! Gondoljuk át milyen

tesztesetekre (jellegzetes bemenő adatokra) érdemes kipróbálni a

programunkat. A fekete doboz teszteseteket a feladat szempontjából

lényeges vagy extrém (szélsőséges) bemenő adatok és a várt eredmények

adják, külön választva ezek közt a feladat előfeltételét kielégítő, úgynevezett

érvényes, és az azon kívül eső érvénytelen tesztadatokat. A fehér doboz

teszteseteket a program kód ismeretében generált tesztadatok alkotják,

amelyek együttesen biztosítják, hogy a kód minden utasítása ki legyen

próbálva, valamint az összefutó és elágazó vezérlési szálak minden

lehetséges végrehajtására sor kerüljön, azaz az úgynevezett szétágazási és

gyűjtőpontjainál mindenféle irányban legalább egyszer keresztülmenjen a

vezérlés. A tesztelést (akárcsak a fordítást, futtatást) menet közben, egy-egy

részprogramra is érdemes már végrehajtani.

A nyomkövetés egy tartalmi hiba bekövetkezésének pontos helyét

segít kideríteni, noha ez nem feltétlenül az a hely, ahol programot javítani

kell. A nyomkövetés a program lépésenkénti végrehajtása, melynek során

például megvizsgálhatjuk a program változóinak pillanatnyi értékét.

Egy program dokumentálása alapvetően két részből áll. A felhasználói

leírás az alkalmazás használatát mutatja be, arról a programot használóknak

ad felvilágosítást. Tartalmazza a megoldott feladat leírását, a program

működésének bemutatását néhány tipikus használati esetre (bemenő

adatkombinációra), a program működéséhez szükséges hardver és szoftver

feltételeket, és a program működtetésének módját (telepítés, futtatás). A

fejlesztői leírás a megoldás szerkezetébe enged bepillantást. Elsősorban

programozóknak szól, és lehetővé teszi, hogy a programot – ha szükséges –

könnyebben lehessen javítani, módosítani. Tartalmazza a feladat szövege

mellett a feladat specifikációját, a program tervét, az implementáció során

hozott, a tervben nem szerepelő döntéseket (beolvasási, kiírási szakasz

megvalósítása, hatékonyság javítása érdekében végzett módosítások), a

Page 16: PROGRAMOZÁSEz 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

16

programkódot (összetett program esetén a részeinek egymáshoz való

kapcsolódását), a tesztelésnél vizsgált teszteseteket. Ezen kívül esetleges

továbbfejlesztési lehetőségeket is megemlíthet. Ebben a kötetben a

feladatok megoldásánál lényegében ez utóbbit, a fejlesztői leírást fogjuk

megadni úgy, hogy kiegészítjük azt a megértést segítő megjegyzésekkel.

Felhasználói leírást a feladatok egyszerű voltára tekintettel nem adunk.

A kötetben negyvenkettő programozási feladat megoldása található. A

megoldásokat tartalmazó fejezeteket úgy alakítottam ki, hogy mindegyik

elején először az érintett feladatok megoldásához köthető implementációs

stratégiákat ismertetem, majd bemutatom a szükséges nyelvi elemeket és

kódolási konvenciókat. A fejezetek ezeket az ismereteket fokozatosan,

egymásra épülő sorrendben vezetik be. A fejezeteket az azokban először

megjelenő C++ nyelvi elemek összefoglalása zárja le.

A kötet három részből áll. Az első rész nagyon egyszerű programok

készítésén keresztül illusztrálja az implementáció folyamatát. Megmutatja,

hogyan kell egy struktogrammot kódolni, hogyan készülnek a programok

beolvasást illetve kiírást végző részei, ha erre a konzolt vagy szöveges

állományt használunk. Megismerjük a tömbök készítésének és használatának

módját. A második rész az alprogramokra (függvényekre, eljárásokra) tördelt

programok készítéséről szól. Először csak az alprogramok használatáról, majd

az első kötetben tárgyalt programozási tételekből származtatott kódok

alprogramba ágyazásáról, ezt követően a több programozási tétellel

megoldott feladat programjának több alprogramra bontásáról, és az

alprogramok csoportjainak külön fordítási egységben, külön komponensben

történő elhelyezéséről, és végül a rekurzívan hívott alprogramokról lesz szó.

A harmadik rész az osztályokat használó programokat mutatja be. Először az

egyszerű osztály fogalmát ismerjük majd meg néhány, az első kötetben

tárgyalt típus megvalósításán keresztül. Majd különféle felsorolók

megvalósításával az első kötet harmadik részének megoldásait

implementálhatjuk. Ez után dinamikus adatszerkezetű típusok osztályait

implementáljuk. Végül megismerjük az osztály-származtatás és a sablon-

pé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.

Page 17: PROGRAMOZÁSEz 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

17

I. RÉSZ

ALAPOK

Ebben a részben egyszerű, kisméretű, egyetlen programblokkban kódolható

konzolalkalmazások készítését vehetjük szemügyre, és ezen keresztül

megfigyelhetjük a programkészítés folyamatát.

Egy egyszerű konzolalkalmazás esetében a kód három szakaszra

tagolódik: bemenő adatok beolvasására és ellenőrzésére; a számítások

elvégzésére; az eredmény megjelenítésére. Ez a felosztás a nagyobb (de nem

objektum orientált) konzolalkalmazásoknál is megfigyelhető.

A számítási szakasz az absztrakt algoritmusnak megfelelő kódot

tartalmazza. Amennyiben az absztrakt program strukturált, akkor annak

szerkezete alapján kívülről befelé haladó kézi fordítással állítjuk elő a kódot.

Mindig az adott szerkezeti elemnek (szekvencia, elágazás, ciklus) megfelelő

utasítást írjuk le, és amikor elemi utasításhoz érünk, azt közvetlenül kódoljuk.

A beolvasási szakaszban gondoskodnunk kell arról, hogy a bemenő

változók megfelelő kezdőértéket kapjanak. A feladat tervéből kiderül, hogy

programunknak melyek a bemenő változói, és azok kezdő értékeire milyen

feltételnek kell teljesülnie. A bemenő adatok kezdőértékeit többnyire a

szabványos bemenetről vagy egy szöveges állományból nyerjük. A beolvasott

adatokra meg kell vizsgálni, teljesítik-e a szükséges előfeltételeket. Ha nem,

akkor hiba-jelzést kell adni, és vagy leállítani a programot, vagy újra meg kell

kísérelni a beolvasást. Előfordulhat, hogy a bemenő adatok beolvasása a

számítási szakasszal összeolvad, mert az adatokat olyan ütemezéssel kell

beolvasni, ahogy azt a számítás igényli.

Az eredmény megjelenítése az eredményváltozók tartalmának

képernyőre, vagy más, a felhasználó által olvasható adathordozóra történő

írásából áll. Itt is előfordulhat, hogy ez a szakasz összeolvad a számítási

szakasszal.

Melyek egy programozási nyelvnek azon részei, amelyeket okvetlenül

meg kell ismernünk az egyszerű feladatok implementáláshoz? Valójában igen

csekély tudás birtokában már fel tudunk építeni egy egyszerű programot.

Page 18: PROGRAMOZÁSEz 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

18

1. A programkód szerkezete

2. Típusok, kifejezések, változók, értékadás

3. Vezérlési szerkezetek

4. Input-output

5. Könyvtári elemek

I-1.ábra. Amit egy programozási nyelvről mindenképpen meg kell tanulnunk

Mindenekelőtt meg kell tanulnunk, hogy az adott nyelven milyen

szerkezeti keretben lehet a kódjainkat elhelyezni. Ez a szerkezet általában

összetett, de szerencsére az első, egyszerű programok elkészítéséhez nem

kell még azt a maga teljességében átlátni.

Már a kezdeteknél tisztázni kell, hogy milyen alaptípusokat (pl. egész,

valós, logikai, karakter) és milyen típusösszetételeket (pl. tömb) ismer az

adott nyelv, hogyan kell leírni e típusok értékeit, hogyan lehet ilyen típusú

változókat deklarálni, milyen műveletek végezhetők ezekkel, milyen

kifejezések építhetők fel belőlük.

Az absztrakt program kódolásához a vezérlési szerkezetek nyelvi

megfelelőit kell megismernünk. Eleinte elég a három vezérlési szerkezetnek

általánosan megfelelő nyelvi elemeket megtalálni, később meg lehet azt

vizsgálni, hogy egy vezérlési szerkezetet speciális esetben milyen sajátos

nyelvi elemmel lehet még kódolni (pl. mikor használjunk for ciklust a while

ciklus helyett, switch-t az if-else-if helyett).

Talán a nyelvek legegyedibb része az, ahogyan a környezetükkel való

kapcsolatot megvalósítják, azaz az input-output utasításai. Az input-output

általában összetett, de mi csak két területével foglalkozunk: a konzolablakos

input-outputtal és a szöveges állományokkal.

Egy nyelv erejét mutatja, hogy támogatja-e korábban megírt kódrészek

újrafelhasználását. Vannak-e olyan úgynevezett kódkönyvtárak, amelyeket

szükség esetén a programunkhoz csatolhatunk azért, hogy azok elemeit

(speciális típusokat, objektumokat, függvényeket) felhasználhassuk. Látni

Page 19: PROGRAMOZÁSEz 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

19

fogjuk, hogy már a legegyszerűbb programok írásánál is szükség lehet ilyen

könyvtári elemekre.

Page 20: PROGRAMOZÁSEz 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

20

1. Első lépések

Ebben a fejezetben nagyon egyszerű (egyszerű programtervvel rendelkező)

feladatok megoldásán keresztül mutatjuk be a programkészítésnek azt a

szakaszát, amelyet megvalósításnak vagy más néven implementálásnak

neveznek. Ezzel párhuzamosan megismerkedünk a C++ programozási nyelv

néhány alapvető nyelvi elemével: az egész számok típusával, változók

deklarálásának módjával, az értékadás utasítással és a szabványos input-

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

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

Page 21: PROGRAMOZÁSEz 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

21

futása, továbbá különleges előírásokat adhatnak a programkód

lefordításához. Ezen nyelvi elemek közé, mint egy keretbe kell majd a kód

többi részét beilleszteni.

A kódnak lényeges részét alkotja a programtervben rögzített absztrakt

programnak az adott programozási nyelvre lefordított változata. Az

implementáció e második lépése a szó szoros értelemben vett kódolás,

amikor a programot egy absztrakt nyelvről (pl. struktogramm leírás) egy

konkrét nyelvre (mondjuk: C++) fordítjuk. Ehhez azt kell tudni, hogy az

absztrakt program utasításainak a programozási nyelv mely utasításai

felelnek meg. Egy utasítás lehet egyszerű: üres lépés vagy értékadás, de lehet

összetett: egy vezérlési szerkezet (szekvencia, elágazás, ciklus). Előfordulhat,

hogy az absztrakt program leírása olyan elemi utasítást tartalmaz, amelyet

nem lehet közvetlenül kódolni a választott konkrét nyelven. Ilyen lehet

például egy szimultán értékadás, amelyik egyszerre, egy időben ad több

változónak is új értéket. Ilyenkor a megvalósítás során a programtervet kell

finomítani, részletezni.

A megvalósítás harmadik és negyedik lépésében az absztrakt program

és a futtató környezet közötti kapcsolatot megteremtő kódot kell kitalálni.

Ennek segítségével a környezetből eljutnak a bemenő adatok a programhoz,

és az eredmény visszajut a környezetbe. Ennek a kódnak általában nincs

absztrakt változata, de a feladat specifikációja kijelöli azokat a változókat,

amelyek kezdő értékeit be kell olvasni a felhasználótól, és azokat, amelyek

értékét meg kell jeleníteni a felhasználó számára. Ezekben a lépésekben

tehát nem abban az értelemben vett kódolás zajlik, hogy egy adott

programot kell egy másik nyelvre átírni, hanem egy sokkal kreatívabb

tevékenység: létre kell hozni olyan kódrészleteket, amelyhez korábbi

tapasztalataink, kódmintáink szolgálhatnak segítségül.

A specifikációban rögzített állapottér megmutatja a bemenő változók

típusát, a specifikáció előfeltétele pedig rögzíti, hogy a bemenő adatoknak

milyen feltételeket kell kielégítenie. Ezek együttesen meghatározzák a

beolvasás módját. A bemenő adatok beolvasásához olyan kódot kell

készítenünk, amelyik megkísérli a kívánt típusú érték beolvasását és ellenőrzi

a szükséges feltételeket. Ha nem sikerül a megfelelő értéket beolvasni, akkor

a kódnak meg kell akadályozni a program további futását. Ehhez kétféle

Page 22: PROGRAMOZÁSEz 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

22

stratégia közül választhatunk. Az egyik megfelelő hibaüzenet generálása után

leállítja a program futását, a másik újra és újra megpróbálja beolvasni az

adatot, amíg az helyes nem lesz. Azt, hogy ezek közül melyiket alkalmazzuk, a

konkrét feladat kitűzése illetve a beolvasandó adat forrása határozza meg.

Például, ha az adatokat közvetlenül a felhasználótól kapjuk, akkor

alkalmazható a második módszer, de ha az adatok egy korábban feltöltött

szöveges állományból származnak, akkor csak az első út járható.

A kiírás figyelembe veszi az eredmény-változók típusát (ez az

állapottérből olvasható ki), valamint a specifikáció utófeltételét. Gyakori eset

például az, hogy egy eredmény-változó csak bizonyos feltétel teljesülése

esetén kap értéket. Ilyenkor a kiírás kódja egy elágazást fog tartalmazni,

amely csak megadott esetben írja ki az eredményt, egyébként pedig egy

tájékoztatást ad annak okáról, hogy az eredmény miért nem jelenhet meg.

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,

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

Page 23: PROGRAMOZÁSEz 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

23

az absztrakt program segédváltozóit, és az implementálás során bevezetett

egyéb segédváltozókat) többnyire (C++-ban mindig) deklarálni kell, azaz

explicit módon meg kell adni a típusukat. Ehhez ismerni kell az adott

nyelvben a deklaráció formáját, elhelyezésének módját, a deklarációnál

használható alaptípusokat és azok tulajdonságait (típusértékeit és

műveleteit). Vannak azonban olyan programozási nyelvek is, ahol a változó

első értékadása az értékül adott kifejezés típusával deklarálja a változó

típusát. A változók deklarációja a megvalósítás ötödik lépése.

Nyelvi elemek

Egy program kódjának sorait az úgynevezett forrásállományba írjuk bele,

amelyet bármilyen szöveges állományt előállító szövegszerkesztővel

elkészíthetünk. Ennek az állománynak általában valamilyen speciális, az adott

programozási nyelvre utaló kiterjesztése van. A C++ nyelvű programjainkat

„.cpp” kiterjesztésű állományokba tesszük. (A gyakorlatban ezen kívül

gyakran előfordul a .c, .C, .cxx kiterjesztés is.) Az egyszerű programok

egyetlen forrásállományban helyezkednek el, az összetett programok kódját

több állományba lehet szétosztani. A forrásállományokban elhelyezett

kódban egyértelműen kell megjelölni azt az utasítást, amelynél a program

végrehajtása (futása) elkezdődik. C++ nyelven ez az úgynevezett main

függvény blokkjának első utasítása. Ezt a blokkot a main függvény nevét

követő nyitó és csukó kapcsos-zárójelek határolják.

A kerethez tartoznak a kód olyan bejegyzései, amelyek a fordító

program számára hordoznak üzenetet. Ilyen például azon kódkönyvtárak

kijelölése, amelynek elemeit fel szeretnénk használni, ezért a kódkönyvtárat

be akarjuk másolni (include) a programunkba. A konzolos input-output

tevékenységekhez például szükségünk lesz egy #include <iostream>

sorra.

Itt kell megemlíteni a minden forrásállomány elején kötelezően

ajánlott using namespace std utasítást is. Számos sokszor használt

előredefiniált azonosító ugyanis az úgynevezett standard (std) névtérben

található. Ez azt jelenti, hogy ezek az előre definiált elemek egy olyan

csomagban találhatók, amelynek elemeire az std:: előtag (úgynevezett

Page 24: PROGRAMOZÁSEz 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

24

minősítés) segítségével lehetne csak hivatkozni, ha nem használnánk a

using namespace std utasítást.

C++ nyelven a main függvényben elhelyezett return utasítás a

program futását állítja le. A main függvény előtti int (integer) szócska utal

arra, hogy a return után egy egész számot (egész értékű kifejezést) kell írni.

Ezt az értéket a program leállása után a futtató környezet (az operációs

rendszer) kapja meg, és tetszés szerint reagálhat rá. Ha ez nulla, akkor az a

„minden rendben” üzenetet szimbolizálja. A return kifejezés helyett

használható még az exit(kifejezés) is, de ehhez bizonyos

környezetekben be kell illeszteni a program elejére egy #include

<cstdlib> sort.

A változó deklarációja a változó típusát írja le. A típus határozza meg,

hogy egy változó milyen értékeket vehet fel és azokkal milyen műveletek

végezhetők. Egy futó program változójához a típusán kívül hozzátartozik a

számítógép memóriájának azon része is, ahol a változó aktuális értéke

tárolódik. A memóriára első megközelítésben gondoljunk úgy, mint bájtok (1

bájt = 8 darab 0 vagy 1-est tároló bit) sorozatára. Minden bájtot

egyértelműen azonosít az ebben a sorozatban elfoglalt pozíciója, azaz

sorszáma, amit a bájt címének hívunk. A sorszámozást nullával kezdjük.

Számok bináris alakja

A számítógépek memóriájában a számokat bináris alakban (kettes

számrendszerben) ábrázoljuk. A bináris alakban felírt szám egy számjegye 0

vagy 1 értékű lehet. Ezt hívjuk bitnek. A számjegy értéke a pozíciójától, a

helyiértéktől függ. A kettes számrendszer helyiértékei a kettő hatványai. Egy

természetes szám bináris alakjának számjegyeit helyiérték szerint növekvő

sorrendben egy kettővel való osztogatásos módszerrel lehet előállítani. Az

osztásoknál keletkezett maradék a soron következő számjegy, az osztás

eredménye pedig a következő osztás osztandója. Az osztogatás addig tart,

amíg az osztandó nem nulla.

Példa: Adjuk meg a 183(10) bináris alakját!

183 : 2 = 91 maradt: 1

91 : 2 = 45 maradt: 1

Page 25: PROGRAMOZÁSEz 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

25

45 : 2 = 22 maradt: 1

22 : 2 = 11 maradt: 0

11 : 2 = 5 maradt: 1

5 : 2 = 2 maradt: 1

2 : 2 = 1 maradt: 0

1 : 2 = 0 maradt: 1

Tehát 183(10) = 10110111(2) = 1*27 + 0*26 + 1*25 + 1*24 + 0*23 + 1*22 + 1*21

+ 1*20

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)

Page 26: PROGRAMOZÁSEz 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

26

Egy változónak csak azt követően lehet értéket adni, hogy kijelöltük a

memóriában a helyét, azaz megtörtént a változó helyfoglalása. Ezt nevezzük

a változó definíciójának. Az értéket bináris kódolással egy vagy több egymás

utáni bájton tároljuk. A tároláshoz szükséges memória szelet első bájtjának

címe a változó címe, a tároláshoz használt bájtok száma a változó mérete.

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

érték

név típus cím lefoglalt memória

szelet

változó regisztráció

Page 27: PROGRAMOZÁSEz 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

27

visszaadott értékeiből álló formula, amely megfelel bizonyos alaki és

jelentésbeli szabályoknak. Alaki (szintaktikai) hiba, például ha egy osztás

műveleti jelhez csak egy argumentumot írunk (/8.2), jelentésbeli

(szemantikai) pedig, például ha két karakterláncot akarunk összeszorozni

(”füzér”*”lánc”), feltéve, hogy nem definiáltuk két lánc szorzását.

Előfordulhat olyan eset is, amikor a kifejezés helyes, de mégsem lehet az

értékét kiszámolni, a program abortál. Gondoljunk például az x/y osztásra, ha

az y változó értéke nulla.

A konzolos input-output műveletek alapszinten lehetőséget adnak

arra, hogy egy bemenő változó a billentyűzetről értéket kaphasson, illetve

egy eredmény-változó vagy eredmény-kifejezés értékét a konzol ablakban

megjeleníthessük. A kódban elhelyezett beolvasás utasításnak tartalmaznia

kell, hogy melyik változónak akarunk értéket adni, a kiíró utasításnak pedig

azt a kifejezést, amelynek az értékét meg akarjuk jeleníteni. Az értékek a

felhasználó számára karakterek sorozatával írhatóak le. Például egy egész

szám egy előjelből (-/+), majd néhány számjegyből áll. A beolvasás során

ezeket a karaktereket a megfelelő billentyűk lenyomásával adjuk meg, és az

<enter> billentyűvel fejezzük be az érték leírását. A begépelt karaktersorozat

a konzolablakban is látható. Kiíráskor az értéket leíró karakterek sorozata fog

a konzol ablakban megjelenni.

C++-ban a beolvasás és a kiírás legegyszerűbb formája az úgynevezett

szabványos input- illetve output adatfolyamok használata. A cin (consol

input) objektumon keresztül a szabványos bemeneten (ez lehet a

billentyűzet) keletkező jeleket (karaktereket) tudjuk beolvasni és adatokká

csoportosítani attól függően, hogy milyen típusú változót kell velük feltölteni.

A beolvasó utasítás formája: cin >> változó. A cout (consol output)

objektumon keresztül a szabványos kimenetre (például a képernyő

úgynevezett konzolablakára) szánt adatot (egy kifejezés értékét)

karaktersorozatra bontva küldhetjük el. A kiírás utasítása: cout <<

kifejezés.

Page 28: PROGRAMOZÁSEz 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

28

1. Feladat: Osztási maradék

Olvassunk be két természetes számot a billentyűzetről, határozzuk meg az

osztási maradékukat, majd az eredményt írjuk ki a konzolablakba!

Specifikáció

A feladatban három darab, természetes számot (nem negatív egészeket)

értékül vevő változó szerepel, ezek között az x és y változók kezdetben

bemenő adatokat tartalmaznak, amelyek rögzített kezdő értékeit jelöljük az

x’-vel és y’-vel. Mivel az x-nek az y-nal vett osztási maradékát (x mod y) kell

kiszámolnunk, az y értéke nem lehet nulla, hiszen a nullával való osztás

eredménye nincs értelmezve.

A = ( x, y, z : ℕ )

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

Page 29: PROGRAMOZÁSEz 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

29

amelyek implementálását a korábban ismertetett öt lépésben végezzük el.

Elkészítjük a program keretét, deklaráljuk a változóit, majd kódoljuk a fenti

három részt.

Program kerete

C++ nyelven a program keretét az a main függvény adja, amelynek

blokkjában (kapcsos zárójelekkel közre zárt részén) elhelyezett utasítások

végrehajtásával kezdődik a program futása. A blokk utasításai

pontosvesszővel vagy csukó kapcsos zárójellel végződnek. Ezek egyfelől

elkülönítik az utasításokat, másfelől a közöttük levő sorrendet is

egyértelműen kijelölik.

Abban a forrásállományban (legyen ennek a neve most main.cpp),

ahol a main függvényt elhelyezzük, még a függvény előtt le kell írnunk két

sort. Az első, az

#include <iostream>

a konzolablakos input/output műveletek definícióját tartalmazó iostream

könyvtári csomagra hivatkozik, a második arra szolgál, hogy cin, cout

azonosítókat ne kelljen std::cin, std::cout formában írni.

#include <iostream>

using namespace std;

int main()

{

...

return 0;

}

Page 30: PROGRAMOZÁSEz 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

30

A pontozott rész helyére kerülnek majd az utasítások. A return 0 a

program futását leállító utasítás. A nulla érték a „minden rendben” üzenetet

szimbolizálja. A main függvény előtti int (integer, egész szám típusú)

szócska a visszatérési érték (ami tehát most a nulla) típusát jelzi.

Deklarációk

C++ nyelven egy változó deklarációja a kód tetszőleges helyén elhelyezett

önálló utasítás:

típus változó

A C++ nyelvben a természetes számok típusának nincs az egész számok

típusától különböző jele. (Az előjel nélküli, unsigned, egészeket mi nem

használjuk.) Több azonos típusú változó deklarációjakor a közös típusnév

után vesszővel elválasztva is felsorolhatjuk a változó neveket.

int x, y, z;

Az egész számok (itt természetes számok) típusa megengedi az

alapműveletek (+, *, -, /) használatát. Az osztás itt az úgynevezett egész-

osztá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.

Page 31: PROGRAMOZÁSEz 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

31

A változó:=kifejezés értékadást a C++ nyelvben változó=kifejezés

formában írjuk. Az értékadás-utasítások után mindig pontosvessző áll

kivéve, ha az nem önálló utasításként, hanem egy kifejezés részeként

jelenik meg.

Az absztrakt programunk C++ nyelvű kódja tehát egyetlen értékadás

utasításból áll, amit az alábbi módon írunk le.

z = x % y;

Bemenő adatok beolvasása

A beolvasás során két problémát is meg kell oldanunk. Egyfelől el kell juttatni

a program futása során a felhasználó által begépelt számokat az x és y

változókba, másfelől ellenőrizni kell, hogy ezek a számok a feladat

szempontjából helyesek-e. Ez utóbbi valójában három féle ellenőrzést jelent.

Egyrészt vizsgálni lehetne, hogy a felhasználó tényleg számot ad-e meg, mert

csak ebben az esetben kerülhet a bemenő változóinkba futási hiba nélkül

érték. (Ilyen ellenőrzést most nem végzünk, de később majd megmutatjuk,

hogyan lehet ezt megtenni.) Másrészt, mivel a változóink egész számokat

tartalmazhatnak, de a feladat csak természetes számokról szól, vizsgálni kell,

hogy a beolvasott számok nem negatívok-e. Harmadrészt pedig a feladat

előfeltételét is ellenőrizni kell, azaz hogy a második szám nem nulla-e.

Számot (de karaktert vagy karakterláncot is) úgy tudunk beolvasni,

hogy az annak értékét leíró (billentyűzeten keletkezett) jelekből képzett

adatot az <enter> billentyű lenyomásának hatására a megfelelő típusú

változóba irányítjuk:

cin >> x

Egy beolvasó utasítás általában nem áll önmagában; megelőzi azt egy

kiírás. Beolvasáskor ugyanis a program futása megszakad és vár az <enter>

billentyű leütésére. Megfelelő tájékoztatás hiányába a felhasználó ilyenkor

nem tudja, mit csináljon, és ezért ő is várni fog, majd arra gondol: „Na, ez a

Page 32: PROGRAMOZÁSEz 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

32

program lefagyott!”. Ezért a beolvasás előtt üzenjük meg a felhasználónak

például a konzolablakba történő kiírással azt, hogy mit várunk tőle.

Programunkban az osztandó beolvasása a következő utasításokkal

végezhető el.

cout << "Kérek egy természetes számot: ";

cin >> x;

Az adatok beolvasása után következik az adatok ellenőrzése. Azzal

tehát most nem foglalkozunk, ha a felhasználó nem egész számot adna meg;

egyelőre feltételezzük, hogy ez történt. Azt viszont megvizsgáljuk, hogy az

első szám nem-negatív, a második szám pedig pozitív. Az ellenőrzéseket

külön-külön, közvetlenül az adott adat beolvasása után végezzük. Ez egy

feltételtől függő tevékenység: ha a vizsgált szám negatív, akkor hibajelzést

írunk a konzolablakba és leállítjuk a program futását (Később ennél

rugalmasabb megoldással is megismerkedünk.) Ellenkező esetben tovább

engedjük a program futását. Ennek leírásához egy elágazás utasítást fogunk

használni.

Az elágazás legegyszerűbb C++ nyelvbeli formája az if utasítás. Az if

kulcsszó utáni kerek zárójelpár közé kell egy logikai értékű kifejezést írni,

amelyik teljesülése esetén (amikor a kifejezés igaz értékű) hajtódik végre a

kerek zárójelpár után írt utasítás, az úgynevezett if ág (más nyelvekben

szokták ezt „then” ágnak is hívni). Ez az utasítás lehet egyszerű (ilyenkor a

végét pontosvessző jelzi), de lehet összetett, több utasítás szekvenciája,

amelyet egy úgynevezett utasítás blokkba (kapcsos zárójelek közé) kell zárni.

Az if ágnak a végrehajtása után a vezérlés többnyire az elágazás utáni

utasításra kerül (hacsak az if ág mást nem ír elő). Ha az elágazás feltétele

nem teljesül (értéke hamis), akkor a vezérlés átugorja az if ágat, és az

elágazás utáni utasításnál folytatódik.

if(feltétel) utasítás; vagy if(feltétel)

{

utasítás1;

Page 33: PROGRAMOZÁSEz 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

33

utasítás2;

...

}

A feltétel egy logikai értéket hordozó kifejezés. Ilyen kifejezés

bármelyik alaptípus értékeiből képezhető, ha azokra alkalmazzuk az

összehasonlítás relációkat. Ilyen az egyenlőség (==), a nem egyenlőség (!=),

a kisebb (<), kisebb-egyenlő (<=), nagyobb (>), nagyobb-egyenlő (>=).

cout << "Kérek egy természetes számot : ";

cin >> x;

if (x<0){

cout << "Negatív számot adott meg! ";

return 1;

}

cout << "Kérek egy pozitív természetes számot : ";

cin >> y;

if (y<=0){

cout << "Nem pozitív számot adott meg! ";

return 1;

}

Miután beolvastuk az x változó értékét, megvizsgáljuk, hogy negatív-e.

Ha igen, akkor hibaüzenetet írunk a konzolablakba és leállítjuk a programot

(return 1). Ez a leállítás visszaad egy hibakódot a futtató környezetnek,

amit az tetszése szerint feldolgozhat. A 0-s hibakód a hibátlan lefutást jelzi

(ez szerepel a program legvégén található return 0 utasításban), az ettől

Page 34: PROGRAMOZÁSEz 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

34

eltérő bármelyik (például az 1-es) kód, valamilyen programhibára utal. Az y

változó értékének ellenőrzésekor a nulla érték is hibásnak számít.

Ha a programunkat egy integrált fejlesztő eszköz keretében futtatjuk,

akkor ott többnyire hozzáépül a programunkhoz egy olyan funkció, amely a

program befejeződése után még nyitva tartja az alkalmazás konzolablakát,

így lesz időnk a legutoljára kiírt üzeneteket, eredményeket elolvasni, és az

ablak csak külön felhasználói beavatkozásra tűnik el. Ha azonban ugyanezt a

programot a fejlesztő környezeten kívül indítjuk el, akkor nem fog működni

ez a funkció: a program befejeződésekor a konzolablak eltűnik. Ez a jelenség

kivédhető, ha a programunk minden kilépési pontjánál elhelyezünk egy

várakozó utasítást. Operációs rendszertől független, tehát általános, minden

környezetben megfelelően működő, ugyanakkor felhasználóbarát várakozó

utasítást nem könnyű találni, a C++ nyelvben sincs ilyen. Van azonban egy

általános, minden környezetben működő bár nem túl elegáns megoldás a

várakozás megoldására.

char ch;

cin >> ch;

Ez a két utasítás egy karakter típusú változóba olvas be értéket

(karaktert), és amíg a felhasználó a karakter begépelése után nem üt

<entert>-t, addig várakozik. Nem túl kényelmes megoldás, hiszen két

billentyű leütése kell hozzá, de univerzális.

Eredmény kiírása

Adatot, egy kifejezés értékét úgy tudunk kiírni, hogy azt a cout-ra irányítjuk.

A z változóban keletkező eredményt a

cout << z

utasítás jeleníti meg. Az eredményt azonban mindig egy megfelelő kísérő

szövegbe ágyazva kell kiírni.

Page 35: PROGRAMOZÁSEz 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

35

Egy szöveges üzenet kiírása a konzolablak aktuális sorába:

cout<<"szöveg". A cout<<endl utasítás hatására a további kiírások a

konzolablak soron következő sorának elejénél folytatódnak. Az endl az std

névtérben definiált speciális jel, amely kiírásával „sort emelhetünk”, azaz

előírhatjuk, hogy a további kiírás a következő sorban folytatódjon.

cout << "első sor" << endl << "második sor";

A rövidebb leírás érdekében a fenti kódrészletet az alábbi formában is

írhatjuk.

cout << "első sor\nmásodik sor";

A \n (újsor – newline) egy speciális, úgynevezett vezérlő karaktert

jelöl, amelynek hatására a további kiírások a konzolablak soron következő

sorának elejénél folytatódnak. Ennek megfelelően a cout<<’\n’ és

cout<<endl utasítások hatása megegyezik.

Az alábbi kódrészletben láthatjuk, hogy több részből álló kiírást hogyan

lehet egyetlen kiíró utasításba befoglalni. Itt a kiírás a balról jobbra sorrend

szerint történik. Megjegyezzük, hogy ezzel a technikával beolvasni is lehet

egyszerre több adatot.

cout << endl << x << " mod " << y << " = " << z;

Tesztelés

A program kipróbálásához kétféle teszteset-csoportot készíthetünk, majd az

egyes esetekre jellemző minta-adatokkal kipróbáljuk a programot. Hibás

működés után, ha a hiba okát felleltük és javítottuk, újra kell kezdeni a teljes

tesztet, hiszen lehet, hogy a javítással elrontottunk olyan részeket, amelyek

korábban már jók voltak.

Fekete doboz tesztelésről akkor beszélünk, amikor a programot a

programkód ismerete nélkül teszteljük, azt vizsgáljuk, hogy a feladat

Page 36: PROGRAMOZÁSEz 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

36

szempontjából helyes-e a program. Ilyenkor a teszteseteket a feladat

szempontjából lényeges vagy extrém adatok kipróbálásának céljából

készítjük. Ezek között megkülönböztetjük az érvényes teszteseteket (amikor

a bemenő adat kielégíti a specifikáció előfeltételét) az érvénytelen

tesztesetektől (amikor azt vizsgáljuk, hogyan viselkedik a program az

előfeltételt nem kielégítő adatokra).

Érvényes tesztesetek:

1. Olyan számpárok, amikor az osztási maradék nulla (Pl: 24, 8 )

2. Olyan számpárok, amikor az osztási maradék egy (Pl: 9, 4 vagy 9, 8

vagy 1, 7)

3. Olyan számpárok, amikor az osztási maradék az osztónál pont eggyel

kisebb (Pl: 9, 5)

4. Olyan számpárok, amikor az osztandó kisebb az osztónál (Pl: 4, 5)

5. Olyan számpárok, amikor az osztandó nulla (Pl: 0, 7)

Érvénytelen tesztesetek:

1. Olyan számpárok, amikor az osztó nulla. (Pl: 15, 0).

2. Mindkét szám nulla.

3. Olyan számpárok, amikor az osztandó negatív. (Pl: -6, 8)

4. Olyan számpárok, amikor az osztó negatív. (Pl: 6, -8)

5. Olyan számpárok, amelyek negatívak. (Pl: -6, -8)

A fehér doboz tesztelés a programkód alapján készül. Mivel a

programunk szerkezete most egyáltalán nem bonyolult, ennek a tesztelésnek

itt nincs nagy jelentősége. Legfeljebb az adatellenőrzési részek elágazásainak

tesztelésével lehetne foglalkozni. Ezeket azonban az érvénytelen tesztesetek

során már kipróbáltuk.

Page 37: PROGRAMOZÁSEz 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

37

Teljes program

Végül nézzük meg teljes egészében a programkódot. Az utasítások között

megjegyzéseket (kommenteket) is elhelyeztünk a könnyebb

áttekinthetőség végett. Megjegyzésnek számít a // utáni sor, illetve a

/* és */ által közrefogott szöveg, amelyeket a fordító figyelmen kívül

hagy.

Látható, hogy az utasítások végét többnyire pontosvessző jelzi.

Az, hogy mi számít utasításnak, mikor lehet a pontosvesszőt elhagyni,

később tisztázzuk. Egyelőre jegyezzük meg azt, hogy a deklarációk,

értékadások, beolvasások és kiírások után, valamint a using

namespace std és return 0 utasítások után mindig van

pontosvessző.

#include <iostream>

using namespace std;

int main()

{

int x, y, z;

// Adatok beolvasása és ellenőrzése

cout << "Kérek egy természetes számot: ";

cin >> x;

if (x<0){

cout << "Negatív számot adott meg! ";

return 1;

Page 38: PROGRAMOZÁSEz 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

38

}

cout << "Kérek egy pozitív természetes számot: ";

cin >> y;

if (y<=0){

cout << "Nem pozitív számot adott meg! ";

return 1;

}

// Számítás

z = x % y;

// Eredmény kiírása

cout << endl << x << " mod " << y << " = " << z;

return 0;

}

Page 39: PROGRAMOZÁSEz 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

39

2. Feladat: Változó csere

Cseréljük ki az egész számokat tartalmazó két változó értékeit! Olvassunk be

két egész számot a billentyűzetről, végezzük el a cserét, majd az eredményt

írjuk ki a konzolablakba!

Specifikáció

A feladatban két egész szám típusú változó szerepel, amelyek egyszerre

bemenő- és eredmény-változók is. A specifikációban lényeges szerepet

játszanak a változók kezdetben megadott értékei (x’ és y’), hiszen így tudjuk a

célfeltételben leírni azt, hogy az x változó az y kezdőértékét, az y változó

pedig az x kezdőértékét veszi fel. A bemenő értékekre semmilyen

megszorítást nem kell tenni.

A = ( x, y : ℤ )

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

Page 40: PROGRAMOZÁSEz 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

40

A programkód előállításánál először elkészítjük a program keretét, majd

deklaráljuk a változókat, kódoljuk az absztrakt programot, amely elé a két

változó kezdő értékének beolvasását végző kód, utána pedig ugyanezen

változók értékét kiíró kód kerül.

Program kerete

#include <iostream>

using namespace std;

int main()

{

...

return 0;

}

Deklarációk

int x, y;

Absztrakt program kódolása

Absztrakt programjainkban gyakran alkalmazunk szimultán értékadásokat.

Ezek egyszerre több változó számára jelölnek ki új értékeket. A C++ nyelvben

ezek közvetlen kódolására nincs lehetőség, azt előbb szét kell bontanunk

egyszerű értékadások egymás után történő végrehajtására, azaz

szekvenciájára. Például az x,y := 3,5 értékadást az x:=3 és y:=5 értékadások

Page 41: PROGRAMOZÁSEz 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

41

tetszőleges sorrendje helyettesítheti. Az x,y:=y,5 értékadás (az x vegye fel y

eredeti értékét, az y új értéke pedig legyen 5) felbontásánál viszont észre kell

vennünk, hogy az y:=5 értékadást csak az x:=y értékadás után szabad

végrehajtani, különben az x nem kapja meg y eredeti értékét, hanem csak az

újat, az 5-öt. Az x:=y értékadás függ az y:=5 értékadástól, pontosabban annak

helyétől.

Az x,y:=y,x szimultán értékadásban az x:=y és az y:=x értékadások

kölcsönösen függenek egymástól. Ezért ahhoz, hogy a szimultán értékadást

egyszerű értékadások szekvenciájára felbontsuk, egy segédváltozót is be kell

vezetni: z:=x; x:=y; y:=z.

Ha speciálisan egész típusú változókról van szó – mint most a példában

– akkor egy másik lehetőség is kínálkozik. Az itt felírt változatok helyességét

az első kötetben tárgyalt módszerrel láthatjuk be.

z := x x := x - y

x := y y := x+ y

y := z x := y - x

Ennek megfelelően az absztrakt program kódja vagy

int z;

z = x;

x = y;

y = z;

vagy

x = x - y;

Page 42: PROGRAMOZÁSEz 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

42

y = x + y;

x = y - x;

lesz. Az első változat a segédváltozó deklarációját is tartalmazza.

Bemenő adatok beolvasása

A beolvasás rész igen egyszerű. Mindössze azt kell egyértelművé tenni, hogy

mikor adunk értéket az első, mikor a második változónak, hiszen az

eredményt csak ennek fényében tudjuk majd értelmezni.

cout << "Az első változó értéke (egész szám): ";

cin >> x;

cout << "Második változó értéke (egész szám): ";

cin >> y;

Adatellenőrzésre most nincs szükség: a feladat előfeltétele ugyanis

nem tett megszorítást és feltételezzük, hogy a felhasználó (megfelelő

méretű) egész számokat fog megadni.

Eredmény kiírása

Az eredmény megjelenítése nem kíván magyarázatot.

cout << "Első változó új értéke: " << x << endl;

cout << "Második változó új értéke: " << y << endl;

Tesztelés

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

Page 43: PROGRAMOZÁSEz 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

43

102), és amikor a második szám a nagyobb. Ezután ebben a programban

külön fehérdoboz teszteseteket már nem kell generálni.

Teljes program

Végül nézzük meg teljes egészében a programkódot megjegyzésekkel és

várakozó utasítással kiegészítve. Absztrakt program kódjaként a második

változat szerepel.

#include <iostream>

using namespace std;

int main()

{

int x, y;

// Adatok beolvasása és ellenőrzése

cout << "Első változó értéke (egész szám): ";

cin >> x;

cout << "Második változó értéke (egész szám): ";

cin >> y;

// Számítás

x = x - y;

y = x + y;

x = y - x;

// Eredmény kiírása

cout << "Első változó új értéke: " << x << endl;

cout << "Második változó új értéke: "

Page 44: PROGRAMOZÁSEz 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

44

<< y << endl;

char ch; cin >> ch;

return 0;

}

C++ kislexikon

Program kerete #include <iostream>

using namespace std;

int main()

{

return 0;

}

Deklaráció típus változó

Értékadás változó = kifejezés

Beolvasás

billentyűzetről

cin >> változó

cin >> változó1 >> változó2

Kiírás képernyőre: cout << kifejezés1 << kifejezés2

cout << endl

Page 45: PROGRAMOZÁSEz 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

45

2. Strukturált programok

Ebben a fejezetben arra szeretnénk rávilágítani, hogy hogyan lehet

minimális, de jól megválasztott programnyelvi készlettel egy tetszőleges

absztrakt programot kódolni. Konkrét feladatok megoldásán keresztül

ismerkedünk meg a C++ nyelv alaptípusaival és vezérlési szerkezeteivel.

Implementációs stratégia

Az elemi programok adott programnyelvre történő leképezése

többnyire igen egyszerű. Az üres (semmit nem csináló, SKIP) program

kódolásához általában semmit nem kell írni; az értékadásnak is minden

nyelven van megfelelője. A szimultán értékadást azonban általában

nem tudjuk közvetlenül kódolni (kivétel például a Python nyelv), ezért

azt előbb át kell alakítani közönséges értékadások szekvenciájára (lásd

előző fejezet 2. feladat), és ha szükséges, ehhez egy vagy több

segédváltozót is bevezethetünk.

Ha a kódolandó programrész egy nevezetes vezérlési szerkezet,

akkor az annak megfelelő nyelvi elemet kell használni. Egy szekvencia

esetén külön kódoljuk a szekvencia egyes tagprogramjait, és azokat

egymás után, a szekvenciában rögzített sorrendben írjuk le.

Elágazásnál az egyes ágakat adó programokat kell külön-külön kódolni,

és azokat az elágazás utasításba beágyazni. Ciklus esetén a ciklusmag

kódját kell a ciklus-utasításban elhelyezni.

d,c := n,m értékadás

c d ciklus-

utasítás

szekvencia

c<d d<c elágazás-

Page 46: PROGRAMOZÁSEz 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

46

d := d–c c := c–d értékadás utasítás

2-4. ábra. Strukturált program kódolása

Egy strukturált program kódolása a benne levő

programszerkezetek mentén, kívülről befelé vagy belülről kifele

haladva történik. A kívülről befele haladó stratégia (top-down) során

az absztrakt programban megkeressük annak legkülső vezérlési

szerkezetét, meghatározzuk e szerkezet komponenseit (szekvencia

esetében az egymást követő részprogramokat, elágazásnál az egyes

ágak programjait és a hozzájuk tartozó feltételeket, ciklusnál a

ciklusmagot és a ciklusfeltételt), és leírjuk az adott szerkezetet kifejező

utasítást. Ennek meghatározott pontjain kell majd a komponensek

kódjait elhelyezni. Ezután a programkomponenseken megismételjük az

előző eljárást. Ha egy részprogram már nem összetett (üres vagy

értékadás), akkor a megfelelő elemi utasítással kódoljuk. A belülről

kifele haladó stratégiánál (bottom-up) éppen az ellenkező irányba

haladunk. Először az absztrakt program elemi utasításait kódoljuk,

majd mindig kiválasztjuk az absztrakt programnak egy olyan vezérlési

szerkezetét, amelynek részprogramjait már kódoltuk, és a nyelv

megfelelő vezérlési utasításával ezeket a részprogramokat egy közös

kódba fogjuk össze.

Az absztrakt programok kódolása során változókat használunk,

ezeket deklaráljuk, értéket adunk nekik és az értéküket felhasználjuk. Ehhez

ismerni kell a használható (megengedett) adattípusokat, azokat a

szabályokat, ahogyan a típusok műveleteinek segítségével kifejezéseket

tudunk szerkeszteni a változókból és a típusértékekből. Ezek a kifejezések

jelennek meg az értékadások jobboldalán, az elágazások és ciklusok

feltételében.

Nyelvi elemek

Page 47: PROGRAMOZÁSEz 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

47

A változó:=kifejezés értékadás C-szerű nyelvekben történő kódolásával az

előző fejezetben már találkoztunk. Fontos, hogy a változó = kifejezés

formájú utasításban a kifejezés értékének típusa meg kell, hogy egyezzen a

változó típusával. Ettől azonban bizonyos esetekben eltekinthetünk. Például

egy egész érték gond nélkül értékül adható valós típusú változónak. C++

nyelven ennek a fordítottja is megtehető, de ilyenkor az értékül adott valós

szám törtrésze elvész, csak az egész része adódik át az egész típusú

változónak. A programozási nyelvek pontosan definiálják a típus-

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

Page 48: PROGRAMOZÁSEz 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

48

be, ráadásul az érintett változó elveszíti a korábbi értékét, hiszen felülírjuk az

eggyel. Ezt a nem várt jelenséget elkerülendő célszerű, ha az az i==1 jellegű

vizsgálatokban a konstanst írjuk baloldalra: 1==i, ugyanis az 1=i kifejezés

esetén biztosan fogunk hibajekzést kapni.

C++ nyelvvel speciális formájú értékadások is írhatók. Önálló

utasításként a ++i vagy az i++ hatása egyenértékű az i = i+1

értékadáséval, a --i vagy i-- hatása pedig az i = i-1 értékadáséval.

Ilyenkor érdemes a prefix változatokat (++i, --i) használni. Ha viszont egy

kifejezésbe ágyazzuk ezeket a speciális értékadásokat, akkor nem mindegy,

hogy melyik alakot használjuk. Az i++ egy kifejezésben az i kezdeti értékét

mutatja, és csak utána fogja megnövelni eggyel. A ++i előbb végzi el az i

növelését, és a kifejezés ezt megnövelt érékét képviseli. (Hasonló a helyzet a

--i vagy i-- értékadásokkal.) Érdekes lehet tudni, hogy az olyan

értékadásokat, mint i=i+a, i=i-a, i =i*a, stb. rövidebb alakban is lehet

C++ nyelven írni: i+=a, i-=a, i*=a, stb.

Megjegyezzük végül azt is, hogy egy változó deklarációja összevonható

azok első (kezdeti) értékadásával. A

típus változó = érték

utasítás a változó definíciójával egyidejűleg kezdőértéket is ad a változónak.

C++ nyelven a változók deklarálása is egy elemi utasítás. Ennek során

foglalódik le a változóban tárolt érték számára a megfelelő memória terület.

Ennek mérete a változó típusától függ. Tekintsük most át a leggyakrabban

használt alaptípusokat: az egész szám, a valós szám, a logikai, a karakter és a

karakterlánc típust.

Az egész szám típusának egyik formájával, az int-tel már

találkoztunk. Ezt használjuk a természetes számok típusának kódolására is.

Az egész számokat többnyire úgynevezett kettes komplemens kódban

ábrázolják a memóriában. Ez a kódolás a kettes számrendszerbeli (bináris)

ábrázoláson túl a negatív számok jelölésének sajátos technikáját jelenti. A

programozási nyelvek (C++ is) több fajta egész típust is bevezet. Ezek abban

különböznek, hogy eltérő méretű memória területet jelölnek ki a változó

számára, így különbözik az általuk ábrázolható legnagyobb és legkisebb egész

Page 49: PROGRAMOZÁSEz 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

49

szám. Olyan egész szám típus nincs, amelyikkel az összes egész számot

ábrázolni lehet, hiszen a memória végessége miatt minden esetben

ábrázolási korlátokba ütközünk. Az, hogy legfeljebb és legalább mekkora

egész számot lehet tárolni egy egész típusú változóban, például az int típus

esetén, az fordítóprogram függő.

Az egész számokra, illetve az egész típusú változókra alkalmazhatjuk az

alapműveleteket (+, -, *, /, %), az összehasonlító relációkat és néhány

beépített függvényt. Ne felejtsük el, hogy két egész szám osztásának

eredménye is egész szám lesz. A műveletek nem minden esetben adnak

helyes eredményt: ha az eredmény nem ábrázolható a választott egész típus

által kijelölt memória területen, akkor túlcsordulás, és ebből fakadó

adatvesztés jön létre.

Page 50: PROGRAMOZÁSEz 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

50

Egész számok kettes komplemens kódja

Az egész számok általánosan elterjedt kódolása. A számokat s biten

ábrázoljuk úgy, hogy a szám abszolút értékének bináris alakja nullával

kezdődjön. Ezért az ábrázolható számok -2s-1 és 2s-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ű:

Page 51: PROGRAMOZÁSEz 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

51

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*2k alakban írjuk fel, ahol b az

előjelbit (0 - pozitív, 1 - negatív), az m a mantissza (1m<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 2s-1–1 eltolású többletes kódjából

(ekkor -2s-1+1k2s-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 2s-1. Ekkor az 100…00 jelenti a nullát és az ábrázolható

számok -2s-1 és 2s-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 2s-1–1-et választjuk, akkor a

nulla kódja 01111111 lesz, az ábrázolható számok -2s-1+1 és 2s-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) *23

Ábrázoljuk a karakterisztikát többletes kódban 11 biten:

3 + 210 –1 (10) = 10000000010(2)

A kód: 1 10000000010 10001000000000000000 … 00000000

Page 52: PROGRAMOZÁSEz 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

52

A double (float, long double) típusú értékekre és változókra

megengedettek az alapműveletek, ezeken kívül a cmath csomagban számos

olyan matematikai függvényt találhatunk, amelyet valós számokra

értelmezhetünk. Ilyen például az sqrt() függvény, amellyel négyzetgyököt

vonhatunk egy nem-negatív valós számból.

A karakter típus jele C++ nyelven a char. Egy karakter-változó

karaktereket vehet fel értékül. A típus értékeit aposztrófok közé írva tudjuk a

kódban közvetlenül leírni. Karaktereket össze lehet hasonlítani az egyenlő

(==) és a nem-egyenlő (!=) operátorokkal. Érvényesek a kisebb és nagyobb

relációs jelek is, de ezek eredménye a vizsgált karakterek kódjától függ.

Karakterek ábrázolása

Egy adott karakterkészlet elemeit, karaktereit megsorszámozunk 0-val

kezdődően, és ezeket a sorszámokat tároljuk binárisan kódolva a megfelelő

karakter helyett. Az egyik legegyszerűbb az ASCII karakterkészlet, amelyik

256 különböző karaktert tartalmaz. A 0 és 255 közötti sorszámok bináris

kódja egy bájton ábrázolható. Az UTF-8 változó hosszú kódolással ábrázolja a

karaktereket (magába foglalja és 1 bájton ábrázolja az ASCII karaktereket, de

például az ékezetes betűk sorszámai 2 bájton kódolódnak.)

A karakterláncok típusa a C++ nyelven a string. Ellentétben a többi

alaptípussal, ennek használatához szükség van az #include <string>

sorra, és a using namespace std nélkül pedig csak std::string

alakban használhatjuk. A típus értékeit idézőjelek közé írt karakterlánccal

írhatjuk le.

A C++ nyelv a C nyelvből fejlődött ki, és ezért számos, a C nyelvben is

használható elemet is tartalmaz. A C nyelvben a karakterlánc egy ’\0’

értékkel lezárt karaktersorozat. A C++ nyelvben e helyett jelent meg a

string típus, de továbbra is használhatók még C stílusú karakterláncok is.

Ennek használata akkor indokolt, amikor olyan műveletekre van szükség,

amelyeket csak a C stílusú karakterláncokra definiál a nyelv. Ilyenkor a C++

stílusú karakterláncot C stílusú karakterlánccá kell átalakítani. Erre szolgál a

c_str() függvény: ha str egy string típusú változó, akkor az

Page 53: PROGRAMOZÁSEz 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

53

str.c_str() kifejezés az str-ben található karakterlánc C stílusú

megfelelője lesz.

Karakterlánc ábrázolása

A karakterlánc egy változtatható hosszúságú karaktersorozat. A karakterek

kódját sorban egymás után tároljuk el. A sorozat hosszát vagy egy külön

számláló vagy a lánc végén utolsó utáni karakterként elhelyezett speciális

karakter (’\0’) jelzi.

A \ karakternek egy karakterláncban általában speciális jelentése van:

az azt követő karakterrel együtt többnyire a karakterlánc megjelenítésével

kapcsolatos tevékenységet vezérli. Ilyen például a \n, amelyik a karakterlánc

kiírásakor nem látszik, de a kiírásnál sortörést eredményez. Vagy a \t, amely

után a kiírás egy tabulátornyival jobbra folytatódik. Ha viszont kifejezetten a

\-t tartalmazó sztringet akarjuk leírni, akkor azt meg kell duplázni:

"c:\\Program Files".

Egy string típusú str változóra nagyon sok művelet alkalmazható.

Lekérdezhetjük a hosszát: str.size() vagy str.length();

lekérdezhetjük vagy megváltoztathatjuk az i-edik karakterét: str[i]; a + jel

segítségével összefűzhetünk két karakterláncot. Lehetőség van a

karakterláncok lexikografikus összehasonlítására is. Egy karakterláncnak ki

lehet hasítani egy darabját is: str.substr(honnan, mennyit). A find()

függvénycsalád (sok változata van) egy sztringben keres karaktert vagy rész-

sztringet, 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 (==, <,

Page 54: PROGRAMOZÁSEz 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

54

<=, >, >=). Ilyen kifejezésekből és logikai változókból összetett logikai

kifejezéseket is szerkeszthetünk. A logikai műveleti jelek eltérő prioritásúak:

a tagadás a legerősebb, a vagy a leggyengébb művelet. Például az u&&!v||w

kifejezés egyenértékű az (u&&(!v))||w kifejezéssel. Ha nem vagyunk

biztosak a prioritási szabályokban, akkor inkább zárójelekkel tegyük magunk

számára egyértelművé a kifejezéseinket.

Logikai érték ábrázolása

Habár a logikai értékek (igaz vagy hamis) tárolására egyetlen bit is elég lenne,

de mivel a legkisebb címezhető egység a memóriában a bájt, ezért legalább

egy bájtot foglalnak el. A nulla értékű (csupa nulla bit) bájt a hamis értéket,

minden más az igaz értéket reprezentálja.

A programozási nyelvek az utasítások végrehajtási sorrendjét

(szekvenciáját) az utasítások egymás utáni elhelyezésével jelölik ki. Az

utasítások végrehajtása ennek a sorrendnek megfelelően, elejétől a végéig,

egymás után szekvenciában történik. Ez egyben azt is jelenti, hogy a

szekvencia vezérlési szerkezetet az őt alkotó programok kódjainak egymás

után írásával tudjuk kifejezni. Több utasítás szekvenciája befoglalható egy

úgynevezett utasításblokkba, amelyet a fordító egyetlen – bár összetett –

utasításként kezel.

Vannak nyelvek, ahol a szekvencia minden tagját új sorban kezdve kell

leírni, de a C++ nyelven ez nem szükséges, hiszen egyértelműen látszik egy

utasításnak a vége, ami egy pontosvessző vagy egy csukó kapcsos zárójel.

Általánosan elfogadott szokás azonban, hogy a szekvencia minden egyes

részét külön sorban kódoljuk. Több utasítás szekvenciáját egyetlen –

összetett – utasításba zárhatunk, ha azt utasításblokként egy nyitó és egy

csukó kapcsos zárójel közé helyezzük. Ezt a blokkot nem követi pontosvessző,

de a blokkbeli utasításokat pontosvessző zárja le. C++ nyelven olyan speciális

utasítások is írhatóak, amelyek ugyancsak szekvenciát kódolnak: ilyen például

a már említett változó1 = változó2 = kifejezés utasítás, amely

először a változó2-nek adja értékül a kifejezés értékét, majd a

Page 55: PROGRAMOZÁSEz 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

55

változó1-nek. Az ilyen rövidítésekkel azonban óvatosan bánjunk, mert már

egy egyszerűbb összetétel értelmezése sem egyértelmű (pl. i = j = i++).

A programozási nyelvek általában rendelkeznek az úgynevezett if-then

elágazás utasítással, (C++ nyelven: if(feltétel) ág ), amely az alábbi

absztrakt program megfelelője:

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,

feltétel

ág SKIP

Page 56: PROGRAMOZÁSEz 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

56

akkor ilyenkor az utasításokat az if illetve else kulcsszóval azonos sorba

írjuk.

if(feltétel) utasítás_1;

else utasítás_2;

Az if-then-else utasítás tehát lehetővé teszi az olyan struktogrammok

kódolását is, ahol egyik ág sem üres program. Egy általános elágazásnak

azonban kettőnél több ága is lehet, és minden ágához egyedi feltétel

tartozik, azaz egy ág nem akkor hajtódik végre, ha egy másik feltétele nem

teljesül. Viszonylag kevés programozási nyelv rendelkezik az ilyen valódi

többágú elágazást kódoló utasítással.

Itt jegyezzük meg, hogy az absztrakt többágú elágazás nem-

determinisztikus (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étel1

ág1

feltétel2

ág2

feltételn

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

feltétel1 feltétel2 ... feltételn

ág1 ág2 ... ágn

Page 57: PROGRAMOZÁSEz 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

57

sem igaz. A két változat csak akkor lesz azonos hatású, ha az elágazás

feltételei teljes és páronként diszjunkt rendszert alkotnak. Ennek hiányában

is igaz azonban az, hogy ha egy feladat megoldásához egy helyesen működő

sokágú elágazást terveztünk, akkor annak a fent bemutatott átalakított

változata is helyesen fog működni.

C++ nyelven a sokágú elágazást egymásba ágyazott „if-else-if”

elágazásokkal kódolhatjuk:

if(feltétel_1){

ág_1

}

else if(feltétel_2){

ág_2

}

else{

á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

Page 58: PROGRAMOZÁSEz 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

58

ciklusmagot. (A kapcsos zárójelpár ugyan elhagyható, ha a ciklusmag

egyetlen utasításból áll, de ezt az írásmódot nem javasoljuk. Ha mégis

megtennénk, akkor ebben az esetben a ciklusutasítást írjuk egy sorba.)

Általában az alábbi írásmódok valamelyikét használjuk:

while(feltétel){ while(feltétel)

mag {

} 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

Page 59: PROGRAMOZÁSEz 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

59

globális változónak, a többit lokálisnak. Ilyenkor abszolút értelemben vett

globalitásról beszélünk.)

Végezetül fontos felhívni a figyelmet arra, hogy a programkódot

megfelelő eszközökkel – C++ nyelven tabulálással (behúzással) ,

kommentekkel (megjegyzésekkel) és üres sorokkal – érdemes úgy tördelni,

hogy ezzel szemléletessé tegyük azt, hogy egy programegység milyen mélyen

ágyazódik egy másikba, ennél fogva könnyen azonosíthatjuk egy változó

láthatósági körét, azaz a változó deklarációját tartalmazó programegységet.

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

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.

Page 60: PROGRAMOZÁSEz 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

60

ilyenkor nem emlékeznek a korábbi értékükre, hiszen nem ugyanaz a

memória szelet foglalódik le számukra.

A C++ nyelv megengedi main függvényen kívül történő változó

deklarációkat is. Ezek a változók globálisak az adott forrásállományban a

deklarációjukat követő minden programegységre nézve, így a main

függvényre nézve is. Ebben a könyvben azonban nem fogunk ilyen változó

deklarációkat alkalmazni.

Page 61: PROGRAMOZÁSEz 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

61

3. Feladat: Másodfokú egyenlet

Oldjunk meg a valós számok halmazán egy másodfokú egyenletet!

Specifikáció

Az ax2+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 a0, 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, x1, x2 : ℝ , válasz : String )

Ef = ( a=a’ b=b’ c=c’ )

Uf = ( a=a’ b=b’ c=c’

(a0 ( b2+4ac < 0 (válasz= „Nincs valós gyök”) )

( b2+4ac = 0 (válasz=„Közös valós gyök”

a

bxx

221

) )

( b2+4ac > 0 (válasz =„Két valós gyök”

a

acbbx

2

42

1

a

acbbx

2

42

2

) ) )

( a=0 ( b0 (válasz=„Elsőfokú gyök” b

cx

1 ) )

( b=0 c=0 (válasz=„Azonosság”) )

( b=0 c0 (válasz=„Ellentmondás”) ) )

Page 62: PROGRAMOZÁSEz 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

62

Absztrakt program

Az utófeltétel első pillantásra talán bonyolultnak tűnik, de aztán

felfedezhető, hogy ebben egy kétágú elágazás van elrejtve, amelynek

mindkét ága egy-egy háromágú elágazásból áll. Ennek megfelelően könnyen

felírható az absztrakt megoldás. A megoldásban megjelenik egy új, a

feladatban nem szereplő valós szám típusú segédváltozó (d) is, amely a

diszkrimináns ideiglenes tárolására szolgál. Vegyük észre, hogy ezt csak az

algoritmus elágazásának egyik ága használja.

a0

acbd 4: 2 b0 b=0 c=0 b=0 c0

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

„Azonos-

ság”

válasz:=„E

llent-

mondás”

x1,x2:=

a

b

2

x1,x2:=

a

db

2

x1:=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

Page 63: PROGRAMOZÁSEz 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

63

A program kerete a korábban megismert forma lesz, amelynek ez elejére a

szabványos input-output műveleteket támogató iostream csomag mellett

matematikai függvényeket definiáló cmath csomag használatát is kijelöltük.

Ez utóbbira a gyökvonás használata miatt lesz szükségünk.

Page 64: PROGRAMOZÁSEz 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

64

#include <iostream>

#include <cmath>

using namespace std;

int main()

{

...

return 0;

}

Deklarációk

Az állapottér öt valós szám típusú változót és egy karakterlánc típusú változót

tartalmazott. Bevezetünk még két logikai változót is (ez egy tipikusan

implementációs döntés) a gyökök számának jelzésére. Ezeket a kiírásnál

fogjuk használni. (A gyökök számának jelzésére használhattunk volna egy

egész típusú változót is, de akkor nem tudtuk volna bemutatni a logikai

változók használatát .) A logikai változóknak a deklarálásuknál adunk kezdő

értéket.

double a, b, c, x1, x2;

string valasz;

bool egy = false, ketto = false;

Absztrakt program kódolása

A kódolandó absztrakt program több, egymásba ágyazott elágazás lesz, de

tartalmaz több szekvenciát is. Alapul véve a tervezésnél felírt

struktogrammot egy kétágú elágazást kell készítenünk, amelynek az első ága

Page 65: PROGRAMOZÁSEz 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

65

egy értékadásnak és egy háromágú elágazásnak a szekvenciája, a második

ága pedig egy újabb háromágú elágazást tartalmaz.

A d segédváltozót ott deklaráljuk, ahol szükség van rá. Hatóköre, azaz

a láthatósága a külső elágazás if ágának végéig tart. Ügyeljünk arra, mikor

kell értékadást (=) és mikor egyenlőséget (==) használni.

Érdemes megfigyelni a programkód vízszintes és függőleges tagolását:

a tabulátorjelek, üres sorok és kommentek használatát.

Page 66: PROGRAMOZÁSEz 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

66

if (a != 0) {

d = b*b-4*a*c; // diszkrimináns kiszámolása

if (d<0){

valasz = " nem rendelkezik valós gyökkel!";

}

else if (0 == d){

valasz = " közös valós gyökei: ";

egy = true;

x1 = x2 = -b/(2*a);

}

else if (d > 0){

valasz = " két valós gyöke: ";

ketto = true;

x1 = (-b+sqrt(d))/(2*a);

x2 = (-b-sqrt(d))/(2*a);

}

} else if (0 == a){

if (b != 0){

valasz = " elsőfokú gyöke: ";

egy = true;

x1 = -c/b;

}

else if (0 == b && 0 == c){

valasz = " azonosság!";

Page 67: PROGRAMOZÁSEz 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

67

}

else if (0 == b && 0 != c){

valasz = " ellentmondás!";

}

}

Megjegyezzük, hogy az egy és ketto változók egyszerre nem lehetnek

igazak.

Bemenő adatok beolvasása

Az adatok beolvasása nem szorul különösebb magyarázatra.

cout << "Másodfokú egyenlet megoldása!\n";

cout << "Az egyenlet ax^2+bx+c=0 alakú.\n";

cout << "Adja meg az egyenlet együtthatóit!\n";

cout << "a = "; cin >> a;

cout << "b = "; cin >> b;

cout << "c = "; cin >> c;

A beolvasott értékeket nem kell ellenőrizni, ha feltételezzük, hogy a

felhasználó tényleg számokat ad meg. Később találkozunk majd olyan

megoldásokkal is, amikor azt is ellenőrizni fogjuk, hogy tényleg számot írt-e

be a felhasználó.

Eredmény megjelenítése

Kiírjuk a válasz-t és a gyököket. Ez utóbbiakat egy háromágú elágazással

annak megfelelően, hogy egyetlen gyök van, két gyök van illetve nincs gyök

Page 68: PROGRAMOZÁSEz 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

68

(ilyenkor semmit nem kell írni, ezért a harmadik ág üres, amely meg sem

jelenik a kódban).

cout << "\nAz egyenlet " << valasz << endl;

if(egy){

cout << "x = " << x1;

}else if(ketto) {

cout << "x1 = " << x1 << endl;

cout << "x2 = " << x2 << endl;

}

Tesztelés

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.

Page 69: PROGRAMOZÁSEz 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

69

9. (1.0,1.0,0.0) Válasz: egy valós gyök:

10. (6.0,-12.0,6.0) Válasz: egy valós gyök: 1.0

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;

Page 70: PROGRAMOZÁSEz 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

70

string valasz;

double d; // diszkriminánst tároló változó

bool egy = false, ketto = false; //gyökök száma

// Beolvasás

cout << "Másodfokú egyenlet megoldása!\n";

cout << "Az egyenlet ax^2+bx+c=0 alakú.\n";

cout << "Adja meg az egyenlet együtthatóit!\n";

cout << "a = "; cin >> a;

cout << "b = "; cin >> b;

cout << "c = "; cin >> c;

// Számolás

if (a != 0)

{

d = b*b-4*a*c; // diszkrimináns kiszámolása

if (d<0){

valasz = " nem rendelkezik valós gyökkel!";

}

else if (0 == d){

valasz = " közös valós gyökei: ";

egy = true;

x1 = x2 = -b/(2*a);

}

Page 71: PROGRAMOZÁSEz 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

71

else if (d > 0){

valasz = " két valós gyöke: ";

ketto = true;

x1 = (-b+sqrt(d))/(2*a);

x2 = (-b-sqrt(d))/(2*a);

}

}

else if (0 == a){

if (b != 0){

valasz = " elsőfokú gyöke: ";

egy = true;

x1 = -c/b;

}

else if (0 == b && 0 == c){

valasz = " azonosság!";

}

else if (0 == b && 0 != c){

valasz = " ellentmondás!";

}

}

// Kiírás

cout << "\nAz egyenlet " << valasz << endl;

if(egy){ // csak egy gyök van

Page 72: PROGRAMOZÁSEz 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

72

cout << "x = " << x1;

}

else if(ketto) { // két különböző gyök

cout << "x1 = " << x1 << endl;

cout << "x2 = " << x2 << endl;

}

return 0;

}

Page 73: PROGRAMOZÁSEz 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

73

4. Feladat: Legnagyobb közös osztó

Olvassunk be két pozitív egész számot billentyűzetről, határozzuk meg a

legnagyobb közös osztójukat, majd az eredményt írjuk ki a konzolablakba!

Specifikáció

A feladatunkban három darab, egész számokat értékül felvevő változó

szerepel, ezek között az m és n változók a bemenő változók. Mindkét

bemenő változó kezdetben pozitív értéket kell, hogy tartalmazzon.

A = ( n, m, d : ℤ )

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 : ℤ) is használ.

d,c := n,m

c d

c<d d<c

d := d–c c := c–d

Page 74: PROGRAMOZÁSEz 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

74

Implementálás

A program implementálását most is öt lépésben végezzük el.

Program kerete

A program kerete a korábban megismert forma lesz.

#include <iostream>

using namespace std;

int main()

{

...

return 0;

}

Deklarációk

A programban használt változók int típusúak.

int m, n, d, c;

Absztrakt program kódolása

Az absztrakt program legkülső szerkezete szekvencia: a d,c := n,m szimultán

értékadásnak és az ezt követő ciklusnak a szekvenciája.

A szimultán értékadást, amely két egymástól független értékadásból

áll, ezért a d = n; c = m; szekvenciával kiválthatjuk. Ezt a két értékadást

azonban a kódban egy sorba írjuk, ezzel kifejezve azt, hogy egymáshoz vett

sorrendjük nem lényeges.

Page 75: PROGRAMOZÁSEz 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

75

A szimultán értékadást szekvenciában követi egy ciklus. A ciklus

feltétel kódját (c!=d) gömbölyű zárójelek között a while kulcsszó után írjuk,

majd egy kapcsos zárójelpárt, amely a ciklusmag helyét jelzi. A ciklusmag egy

kétágú elágazás, ágai értékadások, a kódja egy if-elseif konstrukció, ahol az

if kulcsszót a gömbölyű zárójelpár közé zárt feltétel követi, amelyet a

megfelelő programág (értékadás) kódja követ kapcsos zárójelpár között.

d = n; c = m;

while(c != d){

if (c<d){

d = d-c;

}else if(c>d){

c = c-d;

}

}

Az absztrakt algoritmus kódjában egyáltalán nem kellett volna kapcsos

zárójeleket használni, de így egyrészt jobban kihangsúlyozzuk a

programszerkezetet, másrészt, ha később mégis ki kellene egészíteni újabb

utasítással a ciklusmagot vagy valamelyik ágat, akkor esetleg elfelejtenénk az

utasításblokk utólagos beillesztését, és ez hibás kódhoz vezetne.

Bemenő adatok beolvasása és ellenőrzése

Az adatok beolvasása megfelelő magyarázó szövegek kiírásával jár együtt. A

kódrész végén kerül sor a beolvasott adatok közös ellenőrzésére. A feladat

szerint ezek csak pozitív egész számok lehetnek.

cout << "Legnagyobb közös osztó számítás" << endl;

cout << "Kérem az első számot: "; cin >> m;

Page 76: PROGRAMOZÁSEz 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

76

cout << "Kérem a második számot: "; cin >> n;

if(!(m>0 && n>0)){

cout << "Hiba! Természetes számokkal dolgozok! ";

return 1;

}

Eredmény kiírása

cout << endl << "LNKO = " << d << endl;

Page 77: PROGRAMOZÁSEz 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

77

Tesztelés

A feladat szempontjából készített fekete doboz tesztesetek:

Érvényes tesztesetek:

1. Két nem relatív prím, összetett szám (Pl: 24, 18)

2. Két ugyanazon prímtényezővel rendelkező összetett szám (Pl: 9, 27)

3. Két relatív prím (Pl: 9, 16 vagy 6, 35)

4. Azonos számok (Pl: 4, 4 vagy 3, 3)

5. Két prímszám (Pl: 13, 17)

6. Az 1 és valamilyen más szám (Pl: 1, 18 vagy 1, 24 vagy 1, 1)

7. A kommutativitás vizsgálata (Pl: 1,18 és 18,1)

Érvénytelen tesztesetek:

1. Hibás bemeneti számadatok (<=0) beírása.

Programkód szerinti tesztesetek:

1. Beolvasás

Különböző bemenő adatok (Pl: 6, 10 illetve 10, 6)

A beolvasó elágazás mindkét ágát is kipróbáljuk. (Pl: 6, 8 illetve -3,

5 vagy -4, -7)

2. Főprogram

Főprogram ciklusának ellenőrzése: amikor a ciklus egyszer sem fut

le (Pl: 2, 2), pontosan egyszer fut le (Pl: 2, 4), többször lefut (Pl:

108, 24)

Főprogram ciklusmagjának ellenőrzése: amikor az elágazás

mindkét ága egyszer-egyszer biztosan lefut. (Pl: 6, 8)

Page 78: PROGRAMOZÁSEz 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

78

Teljes program

Tekintsük meg végül a teljes programkódot egyben. A kódot megjegyzésekkel

láttuk el, a számításrész ciklusmagjában a kétágú elágazást tömörebb

formában írtuk le. A program leállásakor a konzolablak eltűnik, ezért az

eredményt csak akkor tudjuk elolvasni, ha a programot vagy egy olyan

környezetben futtatjuk, amelyik a konzolablak megszüntetését csak külön

felhasználói utasításra végzi el, vagy a program végére valamilyen várakozó

utasítást helyezünk el.

#include <iostream>

using namespace std;

int main()

{

int m, n, d, c;

cout << "Legnagyobb közös osztó!" << endl;

//Beolvasás: Az m és n beolvasása (m>0, n>0)

cout << "Kérem az első számot:"; cin >> m;

cout << "Kérem a második számot:"; cin >> n;

if(!(m>0 && n>0)){

cout << " Természetes számokkal dolgozok!";

return 1;

}

Page 79: PROGRAMOZÁSEz 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

79

//Főprogram: d:=lnko(m,n)

d = n; c = m;

while(c != d){

if (c<d) { d = d-c; }

else if(c>d) { c = c-d; }

}

//Eredmény (d) kiírása

cout << endl << "LNKO = " << d << endl;

return 0;

}

Page 80: PROGRAMOZÁSEz 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

80

5. Feladat: Legnagyobb közös osztó még egyszer

Oldjuk meg újra az előző feladatot, de most az Euklideszi algoritmussal!

Változtassunk a specifikáción is: ne válasszuk szét a változóinkat bemenő és

eredményváltozókra, hanem az eredmény az egyik bemenő változóban

keletkezzen! Olvassunk be két pozitív egész számot billentyűzetről,

határozzuk meg a legnagyobb közös osztójukat, majd az eredményt írjuk ki a

konzolablakba!

Specifikáció

A feladatunkban három darab, egész számokat értékül felvevő változó

szerepel, ezek között az m és n változók a bemenő változók. Mindkét

bemenő változó kezdetben pozitív értéket kell, hogy tartalmazzon: n>0

m>0 .

A = ( n, m : ℤ )

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

Page 81: PROGRAMOZÁSEz 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

81

Implementálás

A program megvalósítása most is öt lépésben készül.

Program kerete

A program kerete a korábban megismert forma lesz.

#include <iostream>

using namespace std;

int main()

{

...

return 0;

}

Deklarációk

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;

Page 82: PROGRAMOZÁSEz 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

82

m = s mod m;

}

Megfigyelhető, hogy a segédváltozót a ciklus magjában deklaráltuk. Ez

azt eredményezi, hogy erre a változóra csak a ciklus magjában

hivatkozhatunk, mert ez a változó csak a ciklusmag végrehajtásának kezdetén

jön létre és a ciklusmag befejeződésekor megszűnik. Ez a ciklusmagra nézve

lokális változó.

Ezzel szemben az n és m változók, amelyeket eggyel kijjebb, a main

függvény blokkjában deklaráltunk, a ciklusmag blokkjára nézve globálisak,

használhatóak a ciklusmagban is. Ugyanakkor az n és m változók lokálisak a

main függvény blokkjára nézve, hiszen e blokk végrehajtása során jönnek

létre, és a blokkból való kilépéskor (a program befejeződésekor) szűnnek

meg.

Bemenő adatok beolvasása és ellenőrzése

Az adatok beolvasása megegyezik a 4. feladat megoldásában látottakkal. A

hiba esetén történő leállásához azonban mostantól kezdve nem a return 1,

hanem az exit(1) utasítást használjuk (a visszaadott hibakód lehet más

érték is). A return ugyanis nem lesz alkalmas a több alprogramra bontott

programoknál (lásd II. rész) a meghívott alprogramokban kiváltott leálláshoz.

Ezért célszerű már most átszokni az exit() használatára. Megjegyezzük,

hogy bizonyos környezetekben az exit() értelmezéséhez szükség lehet az

cstdlib modulra, amelyet be kell „inklúdolni” a programunk elejére.

cout << "Legnagyobb közös osztó!" << endl;

cout << "Kérem az első számot:"; cin >> m;

cout << "Kérem a második számot:"; cin >> n;

if(!(m>0 && n>0)){

Page 83: PROGRAMOZÁSEz 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

83

cout << "Természetes számokkal dolgozok!";

exit(1);

}

Eredmény kiírása

Az eredmény kiírás csak abban tér el a 4. feladat megoldásától, hogy itt az n

változó tartalmazza az eredményt.

cout << endl << "LNKO = " << n << endl;

Tesztelés

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:

amikor a ciklus egyszer sem fut le (Pl: 2, 0),

pontosan egyszer fut le (Pl: 2, 2),

többször lefut (Pl: 108, 24)

Teljes program

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;

Page 84: PROGRAMOZÁSEz 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

84

int main()

{

int m, n;

cout << "Legnagyobb közös osztó!" << endl;

//Beolvasás: Az m és n beolvasása (m>0, n>0)

cout << "Kérem az első számot:"; cin >> m;

cout << "Kérem a második számot:"; cin >> n;

if(!(m>0 && n>0)){

cout << "Természetes számokkal dolgozok!";

exit(1);

}

//Főprogram: n:=lnko(m,n)

while(m != 0){

int s = n;

n = m;

m = s % m;

}

//Eredmény (n) kiírása

cout << endl << "LNKO = " << n << endl;

Page 85: PROGRAMOZÁSEz 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

85

char ch; cin >> ch;

return 0;

}

Page 86: PROGRAMOZÁSEz 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

86

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

+ - * /

== != < <= > >=

valós osztás

Logikai bool false,

true

&& || ! == !=

Karakter char ’a’ ’3’

’\’’

== != < <= > >=

Karakterlánc string

”barmi”

” ”

+ [] substr()

size() c_str()

== != < <= > >=

std névtér #include

<string>

Program absztrakt C++

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

elágazás

if (felétel){

„then” ág

}else{

„else” ág

}

elágazás if (felt1){

első program

második program

„else” ág „then” ág

feltétel

ágn

feltn

ág1 ág2 …

… felt2 felt1

Page 87: PROGRAMOZÁSEz 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

87

többágú

ág1

}else if (felt2){

ág2

...

}else if (feltn){

ágn

}

ciklus

while(felétel){

mag

}

feltétel

mag

Page 88: PROGRAMOZÁSEz 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

88

3. Tömbök

A megoldandó feladataink adatai között gyakran szerepelnek tömbök. A

tömb olyan azonos típusú elemek véges sokasága, ahol az elemeket

sorozatba (vektor), téglalapba (mátrix), sorozatok sorozatába (változó

hosszúságú sorokból álló ún. kesztyű mátrix), esetleg még több dimenziós

alakzatba rendezik. Ennek következtében egy tömb egy elemére indexszel, az

elemnek az alakzatban elfoglalt pozíciójának sorszámával (több dimenziós

esetben több indexszel) lehet hivatkozni: kiolvashatjuk illetve

megváltoztathatjuk az így megjelölt értékeit. Egy tömb dimenzióinak száma

is, a dimenziók mérete (hossza) is rögzített, nem változtatható, ennél fogva a

tömb elemeinek száma is állandó.

A tömböket használó absztrakt programok kódolása nem bonyolult,

hiszen a legtöbb magas szintű programozási nyelv biztosítja a tömbök

használatát, és az absztrakt programban használt tömbökkel kapcsolatos

jelölések is hasonlóak a programozási nyelvekéhez. Az absztrakt

programokkal ellentétben azonban a futtatható programban létre is kell

hozni egy tömböt, fel kell tölteni értékekkel, ki kell tudnunk írni ezeket az

értékeket a szabványos kimenetre.

Implementációs stratégia

A tömbök alkalmazása esetén az egyik fontos kérdés az, hogy a választott

programozási nyelv megengedi-e a tömbök elemeinek tetszőleges

indexelését vagy nem. Például a C, C++, C#, Java nyelvekben egy tömb

elemeit csak nullával kezdődően indexelhetjük. Ha az absztrakt programban

használt tömb nem ilyen, akkor az implementálás során átalakítást kell

végezni az absztrakt programon. A fent említett nyelvekben egy m..n

intervallummal indexelt vektort egy 0..n–m intervallummal indexelt egy

dimenziós tömbként lehet csak ábrázolni. Ezért a kódoláskor azokban a

programrészekben, ahol a tömb feldolgozása az m..n intervallumra van

megfogalmazva (például egy ciklusban) vagy át kell térni a 0..n–m

intervallumra, vagy az absztrakt programban i-edik tömbelemként megjelölt

értékre i–m indexszel kell hivatkozni. A beolvasó illetve kiíró részekben a

felhasználó felé viszont mindig a specifikációban szereplő indextartománnyal

Page 89: PROGRAMOZÁSEz 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

89

kell a tömböt megjeleníteni. Tehát ha a kódban szereplő 0..n–m

intervallummal indexelt tömb a specifikáció szerint egy m..n intervallummal

indexelt vektor, akkor a kódbeli tömb i[0..n–m]-edik elemének kiírásakor

azt kell mutatnunk, hogy ez a vektor i+m-edik eleme.

3-1. ábra. Általános indexelésű tömb megfeleltetése 0-tól indexelt tömbnek

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

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

általános tömb:

m i n

C - stílusú tömb:

0 i–m n–m+1

Page 90: PROGRAMOZÁSEz 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

90

feladatban, akkor az absztrakt megoldás kódjában nincs szükségünk a méret-

vá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.

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

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.

Page 91: PROGRAMOZÁSEz 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

91

szeleteken sorban egymás után helyezkednek el a helyfoglalás módjától

függő valamelyik memóriaszegmensben. C++ nyelven a legegyszerűbb mód

egy egydimenziós tömb deklarációjára az alábbi.

int v[34];

Ez egy 34 darab egész szám tárolására alkalmas tömböt foglal le, ahol

az első elem elöl, az utolsó elem hátul foglal helyet. A C-szerű nyelveknél

bevezetett tömb elemeit mindig nullától kezdődően indexeljük, tehát a

példában szereplő tömb első eleme a v[0], utolsó eleme a v[33]. Az, hogy

ez fordítási idejű vagy futási idejű definíciót jelent-e attól függ, hogy hol

helyezzük el. A main függvény törzsében elhelyezve futási időben kiértékelt

definíció lesz, éppen ezért nem kell a méretét konstansként megadni.

int n;

cin >> n;

int v[n];

Erről a megoldásról azonban tudni kell, hogy nem C++ szabvány (csak

C99), viszont a g++ fordító ismeri. A könyv második és harmadik részében

már nem fogjuk használni.

A tömbökkel kapcsolatos legfontosabb művelet az adott indexű elemre

történő hivatkozás. Ha v egy tömb, akkor v[i] a tömb i-edik elemét jelöli:

annak értékét ki lehet olvasni és meg lehet változtatni. A v[i] szerepelhet

értékadás mindkét oldalán illetve kifejezésekben. Fontos tulajdonság még a

tömb mérete, amelyet sok esetben külön kell a programozónak tárolni, mert

nem érhető el közvetlenül. Vigyáznunk kell arra, hogy a tömböt ne indexeljük

túl, azaz ne hivatkozzunk nem létező indexű elemére. Sajnos erre egy C++

kód fordításakor és futtatásakor semmi nem figyelmeztet, csak közvetett

módon a nem várt működés. A programozó feladata, hogy ellenőrizze, hogy

az általa megadott index valóban a tömb valamelyik elemére mutat-e, nem

következik-e be indextúlcsordulás, mint például egy 34 elemű tömb esetében

a v[67] hivatkozás esetén.

Page 92: PROGRAMOZÁSEz 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

92

C++ nyelven változtatható méretű tömböt definiálhatunk a vector<>

típus segítségével. A vector<> típus megvalósításának hátterében

dinamikus memóriakezelés történik, de ennek kódja el van rejtve, azzal nincs

semmi dolgunk, nem kell vele foglalkoznunk. A kisebb-nagyobb jelek között

lehet megadni az elemek típusát (vector<int> vagy vector<double>),

utána a tömbváltozó nevét, és a név után gömbölyű zárójelek között a tömb

méretét, bár ez elmaradhat és később is megadható.

int n;

cin >> n;

vector<int> v(n);

Változók helyfoglalásának módjai

A program változóit meg szokták különböztetni aszerint, hogy

deklarációjukra és helyfoglalásukra rögtön a program futásának kezdetekor

kerül sor (statikus helyfoglalás) vagy a futás közben. Az előbbi esetben a

változó élettartama az egész futási időre kiterjed, az utóbbi esetben csak

annak egy szakaszára. Ha a helyfoglalás a deklarációjának végrehajtásakor

történik, megszűnése az élettartam végét jelző pontnál (például a deklarációt

tartalmazó blokk végén) következik be, akkor automatikus helyfoglalásról

beszélünk. Ha a deklaráció és a helyfoglalás időben különválik, és a

helyfoglalás külön utasításra jön létre és külön utasításra szűnik meg, akkor

dinamikus helyfoglalásról van szó. A program számára kijelölt memória

szegmensek közül a statikus helyfoglalás az úgynevezett adatszegmensben,

az automatikus helyfoglalás a verem szegmensben (stack), a dinamikus

helyfoglalás a szabad vagy dinamikus memóriaszegmensben (heap) történik.

Ezek a tárolási kategóriák a tömbökre is érvényesek. Tömb statikus

helyfoglalásához előre, még a program futtatása előtt, azaz már fordítási

időben ismerni kell a tömb méretét, és mindehhez a tömböt speciális módon

(helyen) kell definiálni. Ennek hiányában (például egy blokkba ágyazott tömb

deklaráció esetén) a tömb automatikus helyfoglalású lesz, ami a deklaráció

végrehajtásakor következik be. Ha még ez előtt lehetőségünk van arra, hogy

Page 93: PROGRAMOZÁSEz 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

93

futási időben kiszámoljuk (esetleg beolvassuk) a tömb méretét, akkor futási

időben megadható automatikus tömb helyfoglalásról beszélhetünk. Tömb

dinamikus helyfoglalása esetén ellenben a programozónak egy úgynevezett

(memória címet tároló) pointerváltozót kell definiálnia először, majd külön

utasítással kell a tömb elemei számara (sorban egymás után) lefoglalni a

szükséges helyet a szabad memóriában, végül e helyfoglalás kezdőcímét a

pointerváltozónak kell értékül adni. Ezt a pointerváltozót kvázi tömbként

használhatjuk, azaz indexelésével hivatkozhatunk a tömb elemeire. A

lefoglalt terület felszabadítása nem feltétlenül automatikus, azt vagy a

felhasználónak kell egy külön utasítással kezdeményezni, vagy bizonyos

nyelveknél a háttérben működő hulladék-gyűjtő mechanizmus (garbage

collector) végzi el.

A vector<> típus számos olyan művelettel is rendelkezik, amelyek

miatt elméleti szempontból már nem is lenne szabad vektornak nevezni.

Ilyen az átméretezést végző resize(), vagy a tömb végéhez új elemet

hozzáfűző push_back() művelet. Lehetőség van egy vektor elemeinek

olyan formában történő indexelésére is, amelyik figyeli az indextúlcsordulást.

A vector-ral létrehozott tömbök hasonlatosak karakterláncokhoz

(string), csak azok kizárólag karaktereket tartalmazhatnak, és olyan

műveleteket is végrehajthatunk, mint például egy részlánc kivágása. A

karakterláncban a karakterekre a tömbökhöz hasonlóan indexeléssel

hivatkozhatunk, és hozzáfűzhetünk, el is hagyhatunk belőle karaktereket.

Többdimenziós tömbök kezelése nem nagyon tér el az

egydimenziósokétól. Ugyanis például egy mátrix felfogható sorok

egydimenziós tömbjének, a sorok pedig maguk is egydimenziós tömbök.

Rögzített méretű n×m-es mátrixot definiál futási időben az alábbi kódrészlet

(nem C++ szabvány):

int n, m;

cin >> n >> m;

int t[n][m];

Page 94: PROGRAMOZÁSEz 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

94

Ugyanez vector<> típussal egy kicsit körülményesebb. Először a sorok

számát kell megadni, majd soronként ez egyes sorok hosszát, viszont

lehetőség van eltérő sorhosszú sorok megadására is:

int n, m;

cin >> n >> m;

vector<vector<int>> t(n);

for(int i=0; i<n; ++i) t[i].resize(m);

A t[i][j] a mátrix i-edik sorának j-edik elemét jelöli, a t[i] a

mátrix i-edik sorát azonosítja.

Az úgynevezett számlálós ciklus egy speciális szerkezetű programot

jelöl. Valójában ez egy szekvencia, amelynek a második része egy olyan

ciklus, amely előre kiszámítható alkalommal futtatja le a ciklus magot. A

számlálós ciklusok egy úgynevezett ciklusváltozót (futóindexet) vezetnek

végig egy egész intervallumon, és sorban annak minden értékére

végrehajtják a ciklusmagot. A ciklusváltozó értéke nemcsak egyesével

változtatható, és készíthető a futóindexet csökkentő számlálós ciklus is.

i:=1

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

Page 95: PROGRAMOZÁSEz 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

95

eleje

feltétel

ciklusmag

továbblépés

A klasszikus számlálós ciklust a for ciklus egy speciális változatával, a

for(int i=1;i<=n;++i){ ciklusmag } utasítással kódolhatjuk. Ebben

a ciklusváltozót a for ciklus belső, lokális változójaként definiálhatjuk.

Természetesen az is könnyen előírható, hogy ne növekedjen, hanem

csökkenjen, és ne egyesével, hanem nagyobb léptékben.

Page 96: PROGRAMOZÁSEz 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

96

6. Feladat: Tömb maximális eleme

Adjunk meg egy tömbben egész számokat, keressük meg a tömb valamelyik

maximális elemét, és az eredményt írjuk ki a szabványos outputra. Készítsünk

többféle megoldást attól függően, hogy a tömböt hogyan hozzuk létre!

Specifikáció

A feladat a tömb elemei felett végzett maximum kiválasztás. Adott tehát egy

egészeket tartalmazó tömb, amelynek elemeit (mivel a feladat

szempontjából ez tűnik logikusnak) 1-től kezdődően indexeljük n-ig, ahol az n

a tömbre jellemző rögzített értékű természetes szám. Az n értéke legalább 1

kell legyen, hiszen a maximum kiválasztás csak akkor értelmezhető, ha a

tömbnek van legalább egy eleme. A célunk az, hogy meghatározzuk a tömb

legnagyobb elemét és ennek az indexét. (Ha a legnagyobb elem több helyen

előfordul a tömbben, akkor mondjuk az első előfordulás indexét keressük.)

A = ( v : ℤn, max, ind : ℤ)

Ef = ( v=v’ n>0 )

Uf = ( v=v’ max = v[ind] = maxn

1i v[i] ind[1..n] )

Az utófeltételben a maxn

1i v[i] jelölés, a {v[1], v[2], … , v[n]} halmaz

legnagyobb elemét adja meg.

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

Page 97: PROGRAMOZÁSEz 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

97

(Ennek a programnak az előállítását könyvünk első kötete

tartalmazza). Vegyük észre, hogy a program egy speciális (úgynevezett

számlálós) ciklusból áll, amelynek segédváltozója az i : ℕ ciklusváltozó. Az 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;

Page 98: PROGRAMOZÁSEz 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

98

}

Absztrakt program kódolása

Az absztrakt program számlálós ciklusának kódoláshoz a for ciklust

használjuk. Ennél figyelembe vesszük azt, hogy a kódban az n elemszámú

tömb indexeinek alsó határa 0, a felső határa pedig az n–1 lesz, szemben az

absztrakt programban használtakkal. Ennek megfelelően a maximum

kiválasztásnál az első megvizsgálandó elem nem a v[1], hanem a v[0] lesz, a

számlálós ciklus intervalluma pedig a 2..n helyett az 1..n–1.

int ind = 0, max = v[0];

for(int i=1; i<n; ++i){

if(v[i]>max){

ind = i; max =v [i];

}

}

Eredmény kiírása

Ha az eredményt kiíró kódban a megtalált elem indexéhez, az ind-hez egyet

hozzáadunk, akkor azt a látszatot kelthetjük, mintha a tömb nem 0-tól

kezdődően lenne indexelve, hanem 1-től. Természetesen a tömb

feltöltésénél is hasonlóan kell majd eljárni: amikor az i+1-dik elemet kérjük,

akkor azt i-edik elemként kell tárolni.

cout << "A tömb legnagyobb eleme: " << max

<< " ,amely a " << (ind+1) << ". elem.\n";

Page 99: PROGRAMOZÁSEz 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

99

Tömb feltöltése

Az alábbiakban többféle tömb megadási módot mutatunk be. Az alábbi

változatok az eddig megadott kódokra nincsenek hatással, azok minden

esetben használhatók.

1. Implementálás konstans tömbbel

A konstans tömb elemeit a program szövegébe ágyazva, fordítási időben

adjuk meg. Ha más adatokkal akarunk dolgozni, akkor módosítanunk kell a

program szövegén, és azt újra le kell fordítanunk.

A v tömb típusú változó definiálásánál meg kell adnunk a tömb

elemeinek a típusát és a tömb elemeinek számát (a tömb hosszát vagy

méretét). A tömb első eleme mindig a 0 indexet kapja, ezen nem tudunk

változtatni. A tömb hossza implicit módon is megadható, ha a definícióban

felsoroljuk a tömb elemeit, ezért nem kell beírni az alábbi kódban a szögletes

zárójelek közé a 8-as méretet. Ez a tömb attól lesz konstans, azaz

megváltozhatatlan, ha a const kulcsszóval jelöljük meg, ami nem engedi

(fordítási idejű ellenőrzés), hogy később megváltoztassuk az elemeit.

const int v[] = {4,7,0,9,6,7,9,4};

A tömb hosszát külön is érdemes eltárolni, ugyanis ez a legegyszerűbb

módja annak, ha a méretre később szükségünk lesz. Erre alkalmas egy

const int n = 8;

konstans értékű segédváltozó, de a programozó felelőssége az, hogy ennek

értéke szinkronban legyen a tömb méretével. Szerencsésebb megoldás, ha a

tömb méretét kiszámoljuk a tömb által lefoglalt teljes memória méret és

bármelyik (például az első) tömbelem által lefoglalt memória méret

hányadosaként. Természetesen ennek csak a v tömb definiálása után van

értelme.

const int n = sizeof(v)/sizeof(v[0]);

Page 100: PROGRAMOZÁSEz 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

100

A maximum kiválasztás esetében figyelni kell arra is, hogy a ciklus

előtti v[0] hivatkozás értelmes legyen. Ezt az előfeltétel ugyan biztosítja, de

ennek a beolvasás részben kell érvényt szerezni. Ezért illesszük be az alábbi

hibakezelő elágazást a kódba.

if (0 == n){

cout << "Nincs a tömbnek eleme!\n";

return 1;

}

Célszerű végül egy for ciklussal kiírni a tömb elemeit a szabványos

kimenetre, hogy a felhasználó lássa a kódba égetett tömb elemeit. Egy n

elemű tömb kiíratásánál a 0..n–1 intervallumot kell egy futóindexszel bejárni,

és minden lépésben a tömb i-edik elemét kiírni. Erre, ha a ciklusmag egyetlen

utasításból áll, akkor használható az alábbi forma is:

for(int i=0; i<n; ++i){ utasítás;}

Ennek megfelelően tehát

cout << "A tömb elemei: " << endl;

for (int i=0; i<n; ++i) cout << v[i] << " ";

cout << endl;

Nézzük meg egyben az egész kódrészletet:

const int v[] = {4,7,0,9,6,7,9,4};

const int n = sizeof(v)/sizeof(v[0]);

if (0 == n){

Page 101: PROGRAMOZÁSEz 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

101

cout << "Nincs a tömbnek eleme!\n";

return 1;

}

cout << "A tömb elemei: " << endl;

for (int i=0; i<n; ++i) cout << v[i] << " ";

cout << endl;

A kitűzött feladat megoldásánál a konstans tömb használata nem

látszik célszerűnek, hiszen annak sem méretét, sem elemeit nem ismerjük a

program megírásakor. A bemutatott megoldás csak didaktikai

megfontolásból került ide.

2. Implementálás maximált méretű tömbbel

Készítsünk olyan tömböt, amelynek elemeit a program futása során kell

beolvasnunk a szabványos bementről. A tömb méretét viszont még fordítási

időben az előtt definiáljuk, mielőtt tudnánk, hogy ténylegesen hány elemet

akarunk elhelyezni benne.

A v tömb típusú változó definiálásánál meg kell adnunk a tömb

elemeinek a típusát és a tömb elemeinek számát. Mivel a pontos

elemszámot nem tudjuk, választunk egy megfelelőnek látszó maximális

méretet, amit a programban konstansként veszünk fel. Ezután deklarálunk

egy ilyen hosszú (pl. 100 elemű), egész számokat tartalmazó tömböt.

const int maxsize = 100;

int v[maxsize];

Ez a deklaráció egyenértékű az int v[100]-zal. A maxsize konstans

használata azért előnyösebb, mert ha a programban mindenhol

következetesen ezt használjuk a 100 érték helyett, akkor egy esetleges

Page 102: PROGRAMOZÁSEz 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

102

program-módosításnál elegendő lesz csak a maxsize kezdőértékét

megváltoztatni.

A v tömbben legfeljebb maxsize darab elem helyezhető el. Ha

kevesebb elemet tárolunk benne, akkor azok a tömb elejére kerülnek.

Ilyenkor szükség van a tömb tényleges hosszának tárolására, ehhez

bevezetünk egy egész típusú változót. Legyen a neve: n. A tömb beolvasása a

tömb tényleges hosszának megadásával kezdődik. Ennek az érteke nem lehet

negatív, és nem lehet nagyobb a maxsize értékénél sem. Ezért a

beolvasáskor a feladat előfeltételéből származó n>0 feltétel mellett az

n<=maxsize feltételt is vizsgálni kell.

const int maxsize = 100;

int v[maxsize];

int n;

cout << "Adja meg a tömb elemszámát, amely 1 és "

<< maxsize << " közé kell essen: ";

cin >> n;

if(!(n>0 && n<=maxsize)){

cout << "Tömb hossza nem megfelelő!\n";

return 1;

}

Ezek után sor kerülhet a tömb elemeinek beolvasására. Látszik, hogy a

beolvasásnál figyelünk a nullától induló indexelésre: az i+1-dik elemet i-edik

elemként tároljuk, így a beolvasáskor a felhasználóban azt a látszatot keltjük,

mintha 1-től indexelt tömbünk lenne. Az alábbi kód feltölti a tömböt a

billentyűzetről. Most nincs arra szükség, hogy külön ki is írjuk a tömb elemeit,

mint azt a konstans tömböknél láttuk, hiszen a konzolablakban még látszanak

a felhasználó által beírt értékek.

Page 103: PROGRAMOZÁSEz 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

103

cout << "Adja meg a tomb elemeit!\n";

for(int i=0; i<n; ++i){

cout << i + 1 << ". elem: ";

cin >> v[i];

}

Az itt bemutatott megoldás rugalmasabb az előzőnél, de van egy nagy

hiányossága: ha több elemet akarunk elhelyezni a tömbben, mint annak

korábban megadott hossza, akkor nem oldható meg a feladat, ha

kevesebbet, akkor feleslegesen foglal memóriahelyet. A következő két

pontban azt mutatjuk meg, hogyan lehet mindig pont akkora tömböt

lefoglalni, amekkorára éppen szükség van.

3. Implementálás futás közben megadott méretű automatikus helyfoglalású

tömbbel

Érdekes lehetőséget kínál a tömbök definiálására az a nem C++ szabvány

szerinti megoldás, amikor a tömb méretét a deklarációban egy futás közben

kiértékelhető kifejezéssel adjuk meg. Erre több féle lehetőség is van. A most

bemutatott változat a legegyszerűbb (erre utal az automatikus helyfoglalású

jelző), de egyszersmind a legkorlátozottabb is.

int n;

cin >> n;

int v[n];

Részletesebben

int n;

cout << "Adja meg a tömb hosszát (1 <= n ): ";

Page 104: PROGRAMOZÁSEz 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

104

cin >> n;

if(!(n>0)){

cout << "Tömb hossza legalább 1 legyen!\n";

return 1;

}

int v[n];

A tömb elemeinek beolvasása azonos a maximált méretű tömbnél

látottakkal.

cout << "Adja meg a tömb elemeit!\n";

for(int i=0; i<n; ++i){

cout << i + 1 << ". elem: ";

cin >> v[i];

}

Fontos megjegyezni, hogy ez a megoldás nem alkalmazható olyan több

függvényre tagolt programok esetén, ahol az egyik függvénynek ez a szerepe,

hogy amikor meghívják, akkor visszaadjon egy ilyen futás közben megadott

méretű tömböt. Ehhez dinamikus helyfoglalású tömb kell, de ezt csak a

pointerek megismerése után tudjuk bemutatni a III. részben. Létezik azonban

egy olyan lehetőség is, amely a dinamikus helyfoglalású tömbök használatát

teszi lehetővé anélkül, hogy a pointerek világában el kellene merülnünk. Erről

szól az alábbi pont.

4. Implementálás vector<>-ral

A legrugalmasabb megoldást kínálja C++ nyelven a szabványos

sablonkönyvtár (STL) vector típusa. Ennek segítségével futási idő alatt

Page 105: PROGRAMOZÁSEz 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

105

adható meg a lefoglalni kívánt tömb mérete, ráadásul egyéb kényelmes

szolgáltatáshoz is hozzásegít: például bármikor lekérdezhető a tömb mérete

vagy megváltoztatható a méret használat közben.

A vector<típus> segítségével egy tetszőleges típusú adott

hosszúságú tömböt tudunk létrehozni,

int n; cin >> n;

vector<int> v(n);

de lehetőség van a méret későbbi megadására is. A

vector<int> v;

egy nulla hosszúságú vektort definiál, amelynek a mérete vagy a

v.resize(új méret) utasítással, vagy v.push_back(elem) utasítással

növelhető. Az előbbi kívánt méretűre módosítja a vektor hosszát, az utóbbi

egy új elemet fűz a végére, azaz eggyel növeli a méretét.

vector<int> v;

int n;

cout << "Adja meg a tömb hosszát (1 <= n ): ";

cin >> n;

if(!(n>0)){

cout << "Tömb hossza legalább 1 legyen!\n";

return 1;

}

v.resize(n);

Page 106: PROGRAMOZÁSEz 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

106

A tömb aktuális mérete bármikor lekérdezhető a size() függvény

segítségével. Ez a függvény egy úgynevezett előjel nélküli egész számként

adja vissza a tömb hosszát, és ennek egész számként való értelmezése

figyelmeztetést vált ki a fordításkor. Ez elkerülhető, ha a size() függvény

értékét egész számmá alakítjuk (explicit konverzió).

int n = (int)v.size();

A tömb elemeinek beolvasása azonos a maximált méretű tömbnél

látottakkal.

int n = (int)v.size();

cout << "Adja meg a tomb elemeit!\n";

for(int i=0; i<n; ++i){

cout << i + 1 << ". elem: ";

cin >> v[i];

}

Tesztelés

A fekete doboz teszteseteket kizárólag a feladat alapján, a megoldó program

ismerete nélkül készítjük, de a tesztelésre csak az implementáció után

kerülhet sor. Itt azokat a bemenő adat-eseteket próbáljuk felderíteni,

amelyek a feladat szempontjából lényegesek illetve szélsőségesek. Az

előbbihez a specifikáció utófeltételét, az utóbbihoz az előfeltételét vesszük

elsősorban figyelembe. Most itt ilyen eseteket sorolunk fel anélkül, hogy egy-

egy esethez konkrét adatértékeket megadnánk.

Érvényes tesztesetek:

1. Nulla darab szám esete.

Page 107: PROGRAMOZÁSEz 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

107

2. Egyetlen szám esete.

3. Sok különböző szám esete.

4. A megadott számok között több azonos forduljon elő a maximális

értékből.

5. Legnagyobb szám az első helyen álljon.

6. Az 4. és 5. esetek ötvözése.

7. Legnagyobb szám az utolsó helyen álljon.

8. Az 4. és a 7. esetek ötvözése.

9. Általános eset.

Érvénytelen teszteset:

1. Nulla vagy negatív hosszú tömb.

A fehér doboz tesztelés azoknak a teszteseteknek az összegyűjtését

jelenti, amelyek azt biztosítják, hogy a program minden utasítása legalább

egyszer végrehajtódjon. Nézzük meg először a tömb feltöltésért felelős

kódot. Ennek tesztelésénél az alábbi esetekre kell tesztadat-sort készíteni:

1. Tömb hosszának helyes megadása. (n=0, n<0 esetek kipróbálása)

2. Egy hosszú tömb esete.

3. Több elemű tömb esete.

4. Maximált méretű tömb használata esetén olyan méretre is ki kell

próbálni a programot, amely eléri illetve nagyobb, mint a

megengedett maximális méret.

A maximum kiválasztás kódjának tesztelésénél olyan adatokat kell

választani, amelyekkel az alábbi futtatások érhetőek el.

5. Egyszer sem lép be a vezérlés a ciklusmagba.

6. Csak egyszer hajtódik végre a ciklusmag.

7. Először a baloldali ága hajtódik végre a ciklus magnak.

8. Először a jobboldali ága hajtódik végre a ciklus magnak.

9. Mindig csak a baloldali ága hajtódik végre a ciklus magnak.

10. Mindig csak a jobboldali ága hajtódik végre a ciklus magnak.

Page 108: PROGRAMOZÁSEz 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

108

Page 109: PROGRAMOZÁSEz 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

109

Teljes program

Tekintsük meg végül egyben a vector<>-t használó teljes programkódot. Itt

nem vezetjük be az aktuális méretet mutató n változót, helyette az

(int)v.size() kifejezést használjuk.

#include <iostream>

#include <vector>

using namespace std;

int main()

{

// Vektor definiálása

vector<int> v;

int n;

cout << "Adja meg a tömb hosszát (1 <= n ): ";

cin >> n;

if(!(n>0)){

cout << "Tömb hossza legalább 1 legyen!\n";

return 1;

}

v.resize(n);

// A vektor elemeinek feltöltése

cout << "Adja meg a tömb elemeit!\n";

Page 110: PROGRAMOZÁSEz 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

110

for (int i=0; i<(int)v.size(); ++i){

cout << i + 1 << ". elem: ";

cin >> v[i];

}

// Maximum kiválasztás

int ind = 0, max = v[0];

for(int i=1; i<(int)v.size(); ++i){

if (v[i]>max){

ind = i; max = v[i];

}

}

// Eredmény kiírása

cout << "A tömb legnagyobb eleme: " << max;

cout << " ,amely a " << (ind+1) << ". elem.\n";

return 0;

}

Page 111: PROGRAMOZÁSEz 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

111

7. Feladat: Mátrix maximális eleme

A földfelszín egy téglalap alakú darabján megmértük a tengerszint feletti

magasságokat, és azokat egy mátrixban rögzítettük. Melyik a terület

legmagasabb pontja, azaz melyik a mátrix legnagyobb eleme, hányadik

sorban és oszlopban található?

Specifikáció

A feladat hasonlít az előzőre, de most egy kétdimenziós alakzatban kell

maximumot keresni.

A = ( t : ℤn×m, max, ind, jnd : ℤ )

Ef = ( t=t’ n>0 m>0 )

Uf = ( t=t’ max = t[ind,jnd] = ],[,

,jit

mn

1j1imax

ind[1..n] jnd[1..m] )

Absztrakt program

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.

Page 112: PROGRAMOZÁSEz 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

112

#include <iostream>

#include <vector>

using namespace std;

int main()

{

// Mátrix definiálása és feltöltése

...

// Maximum kiválasztás

...

// Eredmény kiírása

...

return 0;

}

Mátrix definiálása és feltöltése

Nemcsak egydimenziós tömböket lehet C++ nyelven definiálni, hanem két-

(mátrixok), sőt többdimenziósakat is. Amennyiben a többdimenziós tömb

fogalmával tisztában vagyunk, akkor a C++ nyelvű használatuk nem jelenthet

gondot. Például az int t[n][m]utasítás egy n×m-es egész számokat

tartalmazó mátrixot definiál. A mátrix méretei futási időben is megadhatók.

A mátrix i-dik sorának j-dik elemét jelöli a t[i][j] szimbólum, de

lehetőségünk van t[i]-vel a teljes i-edik sorra hivatkoznunk. A mátrix

feltöltését és kezelését két egymásba ágyazott for ciklussal végezzük.

A vector<> típus segítségével is lehet mátrixokat definiálni. Sőt, ez

arra is lehetőséget ad, hogy változó sorhosszúságú, úgynevezett kesztyű

mátrixokat hozzunk létre (igaz, ebben a feladatban téglalap alakú mátrixra

Page 113: PROGRAMOZÁSEz 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

113

van szükség), mert a kesztyűmátrix különböző méretű vektoroknak a vektora.

Egy n×m-es téglalap alakú mátrixot hoz létre az alábbi kódrészlet.

int n, m; cin >> n >> m;

vector< vector<int> > t(n);

for(int i = 0; i<(int)t.size(); ++i)

t[i].resize(m);

Az is látható a fenti kódból, hogy ha akarnánk, hogyan adhatnánk

minden sornak más-más méretet.

A mátrix feltöltése két egymásba ágyazott for ciklussal történik. A

ciklusfeltételhez a size() függvénnyel kérdezzük le a tömb méreteit.

Ügyelünk arra is, hogy bár a C++ 0-tól kezdődően indexeli a tömböket, a

felhasználó egy 1-től indexelt mátrixot lásson.

for(int i = 0; i<(int)t.size(); ++i)

for(int j = 0; j<(int)t[i].size(); ++j) {

cout << "t[" << i+1 << "," << j+1 << "]= ";

cin >> t[i][j];

}

Maximum kiválasztás

Az absztrakt program kódjában a mátrix sorainak és oszlopainak

indextartományát át kell alakítani a 0-től indexelt mátrixra. Formailag

ugyanaz a két ciklus szerepel itt is, mint a mátrix feltöltésénél. ( A beolvasás

és a maximum kiválasztás dupla ciklusait össze is lehetne vonni, de akkor

tömbre sem lenne szükség, márpedig itt ennek a használatát mutatjuk be.)

int ind = 0, jnd = 0, max = t[0][0];

Page 114: PROGRAMOZÁSEz 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

114

for(int i = 0; i<(int)t.size(); ++i)

for(int j = 0; j<(int)t[i].size(); ++j) {

if(t[i][j]>max){

ind = i; jnd = j; max = t[i][j];

}

}

Eredmény kiírása

A kiírásnál ügyelünk arra, hogy a felhasználó 1-től indexelt mátrixot lásson.

cout << "A mátrix legnagyobb eleme: " << max

<< " ,amely a " << ind+1 << ". sor "

<< jnd+1 << ". eleme.\n";

Tesztelés

Az előző feladat tesztelése alapján készíthetők erre a megoldásra is

tesztesetek. Ezt az Olvasóra bízzuk.

Teljes program

Tekintsük meg végül egyben a teljes programkódot.

#include <iostream>

#include <vector>

using namespace std;

Page 115: PROGRAMOZÁSEz 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

115

int main()

{

// Mátrix definiálása

int n, m;

cout << "A mátrix sorainak száma (1 <= n ): ";

cin >> n;

cout << "A mátrix oszlopainak száma (1 <= m ): ";

cin >> m;

if(!(n>0 && m>0)){

cout << "Méret legalább 1×1-es legyen!\n";

return 1;

}

vector<vector<int> > t(n);

for(int i=0; i<n; ++i) t[i].resize(m);

// A mátrix elemeinek feltöltése

cout << "Adja meg a mátrix elemeit!\n";

for(int i = 0; i<(int)t.size(); ++i)

for(int j = 0; j<(int)t[i].size(); ++j) {

cout << "t[" << i+1 << "," << j+1 << "]= ";

cin >> t[i][j];

}

// Maximum kiválasztás

int ind = 0, jnd = 0, max = t[0][0];

Page 116: PROGRAMOZÁSEz 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

116

for(int i = 0; i<(int)t.size(); ++i)

for(int j = 0; j<(int)t[i].size(); ++j) {

if(t[i][j]>max){

ind = i; jnd = j; max = t[i][j];

}

}

// Eredmény kiírása

cout << "A mátrix legnagyobb eleme: " << max

<< " ,amely a " << ind+1 << ". sor "

<< jnd+1 << ". eleme.\n";

return 0;

}

Page 117: PROGRAMOZÁSEz 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

117

8. Feladat: Melyik szóra gondoltam

„Találd ki melyik szóra gondoltam!” A programot két felhasználó kezeli. Az

egyik, ő a játékmester, kitalál egy szót, és azt begépeli úgy, hogy az a másik

felhasználó számára rejtve maradjon. A másik felhasználó, a játékos,

megpróbálja ezt a számára elrejtett szót kitalálni úgy, hogy egymás után

többször is tippel, az alkalmazás pedig jelzi, hogy mely betűket sikerült

eltalálnia. A program a játékos tippjére egy olyan szóval válaszol, amely

azokon pozíciókon, ahol a tipp és a rejtett szó betűje megegyezik, az adott

betűt mutatja, a többi pozíción pedig csak egy-egy pontot jelenít meg. A

program számolja azt is, hogy hány próbálkozásból sikerült megfejteni a

rejtett szót!

Specifikáció

A feladat megoldása azzal kezdődik, hogy beolvassuk a játékmestertől az

elrejtendő szót. Ekkor inicializáljuk a próbálkozások számát nullára.

Ezt követően ugyanannak a tevékenységnek az ismétlése következik.

Ez a tevékenység az alábbi feladatot oldja meg: Olvassunk be egy

karakterláncot (hívjuk ezt tippnek) a játékostól. Növeljük meg a

próbálkozások darabszámát eggyel. Ha a tipp megegyezik az előzetesen

elrejtett szóval vagy egy speciális, a játék feladását jelentő ”x” üzenettel,

akkor ezt visszaigazoljuk. Ha a tipp hosszabb, mint a rejtett szó, akkor vágjuk

le a végét; ha rövidebb, egészítsük ki annyi ponttal, hogy a hossza

megegyezzen a rejtett szó hosszával. Ezután a tipp minden olyan betűje

helyére is írjunk pontot, amelyik nem azonos a rejtett szó ezen pozícióján

levő betűvel.

Specifikáljuk azt a résztevékenységet, amely feltételezi, hogy ismert a

rejtett és a tippelt szó, az eddigi próbálkozások száma, és előállítja a tipp

helyén a választ, növeli a próbálkozások számát feltéve, hogy a játékos nem

akarta abbahagyni a játékot. Egy logikai változó jelezze a játék végét, amely

lehet sikeres, lehet sikertelen.

A = ( rejt, tipp : String, darab : ℕ, ki : 𝕃 )

Ef = ( rejt=rejt’ tipp=tipp’ darab=darab’ )

Uf = ( rejt=rejt’ darab=darab’+1

Page 118: PROGRAMOZÁSEz 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

118

ki = (tipp’ = rejt tipp’ = ”x”)

ki tipp = rejt

i[1..rejt]: (tipp’[i]rejt[i] tipp[i]= ”.”)

(tipp’[i]=rejt[i] tipp[i]= tipp’[i]) )

Ezt a résztevékenységet egy ciklusba kell majd szervezni, ami addig

tart, amíg a ki változó nem lesz igaz. Majd ezután megfelelő üzenetet írunk ki

attól függően, hogy a játékos eltalálta a rejtett szót vagy feladta a játékot.

Absztrakt program

A résztevékenységet megoldó programot annak utófeltétele alapján könnyen

elkészíthetjük.

darab:=darab+1

ki := (tipp = rejt tipp = ”x”)

ki

tipp := rejt

SKIP i = 1 .. rejt

tipp[i] rejt[i]

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

Page 119: PROGRAMOZÁSEz 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

119

megelőző beolvasásából (az aktuális tippet vagy a kilépési szándékot kell itt

megkérdeznünk) valamint az azt követő (a próbálkozás eredményének)

kiírásból áll. Ezt ágyazzuk be a külső szintbe, amely kezdeti értékadásokkal

kezdődik (a rejtvény bekérése, a próbálkozások darabszámának lenullázása, a

kilépést jelző logikai változó hamisra állítása), majd a belső szint ciklikusan

ismételt végrehajtása következik, végül az eredmény kiírására kerül sor.

Belső szint implementálása

Az absztrakt program kódolása önmagában nem jelenthet gondot. A

vezérlési szerkezetek kódolását ugyanis már ismerjük, legfeljebb a

karakterláncok (sztringek) használatával kell megbirkóznunk.

Egy karakterlánc hasonlít egy karaktereket tartalmazó tömbre,

amennyiben egy sztring i-edik karakterére a tömböknél látott indexeléssel

hivatkozhatunk. A tipp kiértékelését végző ciklus kódolásához szükség van a

karakterlánc hosszának ismeretére (size()).

for(int i=0; i<(int)rejt.size(); ++i)

if(tipp[i] != rejt[i]) tipp[i] = '.';

A tipp:=rejt értékadás megvalósításához egyfelől le kell vágnunk a

tipp hosszából, ha az túlnyúlna a rejtvény hosszán, másfelől meg kell

nyújtani, ha rövidebb lenne, mint a rejtvény. Egy sztringhez hozzáfűzhető egy

másik sztring, kereshetünk benne adott részsztringet vagy karaktert,

kivághatunk belőle egy részt.

A substr() függvény segítségével egy karakterláncnak egy részét,

megadott pozíciótól kezdődő megadott darabszámú karakterét tudjuk

kivágni. A tipp.substr(0,n) a tipp karakterláncnak n hosszú elejét

(nullával kezdődik egy sztring indexelése) kapjuk meg. Ha az n hosszabb, mint

a tipp hossza, akkor a visszakapott rész-sztring maga a tipp lesz.

tipp = tipp.substr(0, (int)rejt.size());

Egy sztringhez a + operátorral fűzhetünk hozzá másik sztringet. Például

str+"bővítmény". Az str=str+"bővítmény" értékadást

Page 120: PROGRAMOZÁSEz 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

120

str+="bővítmény" alakban rövidíthetjük. Egyformán működik az

str=str+"y" és az str=str+’y’, mert a sztringhez a + operátorral

nemcsak sztringeket, hanem karaktereket is hozzá lehet fűzni. Általában

azonban meg kell különböztetnünk az egyetlen karakterből álló

karakterláncot a karaktertől. Ügyeljünk arra, hogy a sztringeket idézőjelek

közé, a karaktereket aposztrófok közé kell írni, és ne lepődjünk meg, hogy az

’y’=="y" vizsgálat szintaktikailag helytelen. A nyújtás során olyan

karaktereket (például pontokat) kell hozzáírnunk a tipphez, amelyek biztosan

nem egyeznek meg a rejtvénybeli adott pozíciójú karakterekkel hiszen azokat

– mivel egyáltalán nem szerepelt a tippünk ezen pozícióin karakter – nem

találtuk el a rejtvényből.

for(int i=(int)tipp.size();

i<(int)rejt.size();++i)

tipp += '.';

Nézzük most meg egyben a belső szint kódját.

// Játékos szándéka

cout << "\nHa kilép, írjon be egy x-et. "

<< "Ha nem, tippeljen!\n";

cout << "Tipp = "; cin >> tipp;

if(tipp != "x") ++darab;

ki = tipp == rejt || "x" == tipp;

if(!ki){

// |tipp| := |rejt|

tipp = tipp.substr(0, (int)rejt.size());

Page 121: PROGRAMOZÁSEz 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

121

for(int i=(int)tipp.size();

i<(int)rejt.size(); ++i) tipp += '.';

// tipp kiértékelése

for(int i=0; i<(int)rejt.size(); ++i)

if(tipp[i] != rejt[i]) tipp[i] = '.';

cout << endl << darab << ". próbálkozás = "

<< tipp << endl;

}

Az absztrakt program kódját el kell látni megfelelő beolvasás és kiírás

részekkel is. Az előfeltételben szereplő bemenő változók közül itt az absztrakt

program előtt csak a játékos soron következő tippjének beolvasását kell

elvégezni, a rejtvényt csak a legelső próbálkozás előtt, a külső szinten

olvassuk be, és ugyanott nullázzuk le a próbálkozások darabszámát is, amely

minden menetben eggyel nő. A tipp kiértékelése után pedig – ha nem akart a

játékos kilépni a játékból – megjelenítjük, hogy hányadik próbálkozásnál

járunk, és kiírjuk a kiértékelt tippet.

Külső szint implementálása

A belső szint kódját egyszer mindenképpen, de többnyire sokszor egymás

után kell végre hajtani. Ezért ezt egy ciklusba ágyazzuk, ráadásul úgy, hogy

annak a magja egyszer biztosan végrehajtódjon. Sok programozási nyelvben

található olyan ciklusutasítás, amely éppen így működik; ezt hívják hátul

tesztelő ciklusnak.

A hátul tesztelős ciklust C++ nyelvben a do-while utasítással írhatjuk le.

A do{ciklusmag}while(feltétel) az alábbi programszerkezetnek felel

Page 122: PROGRAMOZÁSEz 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

122

meg. A do-while utasítás alkalmazásával a ciklusmagot csak egyszer kell

kódolni.

ciklusmag

feltétel

ciklusmag

E ciklus előtt azonban ne feledkezzünk meg a rejtvény beolvasásáról és

a próbálkozások darabszámának nullára állításáról, utána pedig az eredmény

megjelenítéséről.

cout << "Adja meg a rejtett szót: ";

cin >> rejt;

int darab = 0;

do{

// Belső szint

}while(!ki);

if(tipp == rejt)

cout << "\nGratulálok! " << darab

<< ". tippel nyert!\n";

else if ("x" == tipp)

cout << "\nÖn vesztett! Majd legközelebb.\n";

Page 123: PROGRAMOZÁSEz 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

123

Page 124: PROGRAMOZÁSEz 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

124

Tesztelés

A fekete doboz teszteseteket két csoportba soroljuk. Egyrészt teszteljük a

specifikációban részletezett részfeladat megoldó programját, másrészt azt a

program környezetet, amelyik ezt a részt újra és újra lefuttatja, előtte

inicializálja a játékot, a végén pedig kiírja az eredményt. Érvénytelen

tesztesetek nincsenek.

Az első csoport tesztesetei:

1. Olyan tipp, amelyik egyáltalán nem illeszkedik a rejt -hez.

2. Olyan tipp, amelyik csak a már ismert karaktereknél illeszkedik a

rejt-hez.

3. Olyan tipp, amelyik a már ismert karaktereknél sem mindig

illeszkedik a rejt-hez.

4. Olyan tipp, amelyik egyetlen újabb karaktert talál el rejt-ből.

5. Olyan tipp, amelyik több újabb karaktert talál el rejt-ből.

6. Olyan tipp, amelyik minden karaktert eltalál a rejt-ből.

7. Üres rejt vagy tipp karakterlánc esetei.

8. Eltérő hosszú rejt vagy tipp karakterlánc esetei.

9. Próbálkozások darabszáma növelésének ellenőrzése.

A második csoport tesztesetei:

1. Sikeres eredmény kijelzése egy, kettő, több próbálkozásból.

2. Sikertelen eredmény kijelzése kilépés esetén egy, kettő, több

próbálkozásból.

3. Próbálkozások darabszáma kezdeti értékének és végeredményének

ellenőrzése, ha csak egy, kettő, több próbálkozás volt.

A fehér doboz tesztelés a fenti tesztesetek lefedik, így újabb

tesztesetekre nincs szükség.

Page 125: PROGRAMOZÁSEz 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

125

Teljes program

Végül nézzük meg teljes egészében a programkódot.

#include <iostream>

#include <string>

using namespace std;

int main()

{

// Rejtvény beolvasása

string rejt;

cout << "Adja meg a rejtett szót: ";

cin >> rejt;

int darab = 0;

// Próbálkozások a rejtvény kitalálására

string tipp;

bool ki;

do{

// Játékos szándéka

cout << "\nHa kilép, írjon be egy x-et. "

<< "Ha nem, tippeljen!\n";

cout << "Tipp = "; cin >> tipp;

if(tipp != "x") ++darab;

ki = tipp == rejt || "x" == tipp;

Page 126: PROGRAMOZÁSEz 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

126

if(!ki){

// |tipp| := |rejt|

tipp = tipp.substr(0, (int)rejt.size());

for(int i=(int)tipp.size();

i<(int)rejt.size(); ++i) tipp += '.';

// tipp kiértékelése

for(int i=0; i<(int)rejt.size(); ++i)

if(tipp[i] != rejt[i]) tipp[i] = '.';

cout << endl << darab << ". próbálkozás = "

<< tipp << endl;

}

}while(!ki);

// Eredmény kiírása

if(tipp == rejt)

cout << "\nGratulálok! " << darab

<< ". tippel nyert!\n";

else if ("x" == tipp)

cout << "\nÖn vesztett! Majd legközelebb.\n";

return 0;

}

Page 127: PROGRAMOZÁSEz 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

127

C++ kislexikon

vektor

definiálása

konstans const Element v[] = { … , … , };

const int size =

sizeof(v)/sizeof(v[0]);

konstans

méretű

const int maxsize = 100;

Element v[maxsize];

int size;

… // size értéke: 0≤size≤maxsize

rögzített

méretű

int size;

… // size értéke: size≥0

Element v[size]; //NEM C++

vector<> #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;

Page 128: PROGRAMOZÁSEz 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

128

… // sizei értéke: 0≤sizei≤maxi

// sizej értéke: 0≤sizej≤maxj

rögzített

méretű

int sizei, sizej;

… // sizei értéke: 0≤sizei

// sizej értéke: 0≤sizej

Element a[sizei][sizej]; //NEM C++

vector<>

(téglalap

mátrix)

#include <vector>

int sizei, sizej;

… // sizei értéke: 0≤sizei

// sizej értéke: 0≤sizej

vector<vector<Element>> a(sizei)

for(int i=0,i<sizei,++i)

t.resize(sizej);

mátrix

feltöltése

for(int i=0; i<sizei; ++i){

for(int j=0; i<sizej; ++j){

cin >> a[i][j];

}

}

Page 129: PROGRAMOZÁSEz 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

129

4. Konzolos be- és kimenet

Absztrakt programjaink tervezésekor feltételezzük, hogy a bemenő adatok

értékeit a szükséges ellenőrzések után már az úgynevezett bemenő változók

tartalmazzák, az eredmény pedig az eredmény változókba kerül. Egy

futtatható program azonban nem állhat kizárólag az absztrakt program

kódjából, hiszen olyan részeket is tartalmaznia kell, amelyek a bemenő

adatokat a felhasználótól a bemenő változókba juttatják el, közben elvégzik

az ellenőrzéseket, és az eredményt a felhasználó számára is érthető

(olvasható) formában jelenítik meg.

Annak, hogy a felhasználó adatokat adjon meg egy programnak és

adatokat kapjon tőle, több módja is van. Lényegesen korlátozza a

lehetőségeinket az a tény, hogy ebben a könyvben nem grafikus felhasználói

felületet használunk begépelt adatok beolvasásához illetve az eredmény

kiírásához, hanem vagy konzolablakos technikát, vagy szöveges

állományokat. (Ez utóbbival a következő fejezetben foglalkozunk.) Ez egy

szöveges alapú kommunikáció a felhasználó és a program között. Legkisebb

egységei a karakterek, amelyeket sorban egymás után lehet csak beírni vagy

megjeleníteni, és nincs lehetőség egy korábban be- vagy kiírt karakterhez

visszatérni, azt módosítani.

Implementációs stratégia

Egy alkalmazás be- és kimeneti (input-output) tevékenysége nem kizárólag a

bemenő adatok beolvasásából és az eredmény adatok kiírásából áll. Nagyon

fontos, hogy ez olyan körítésben, olyan tájékoztató üzentekkel együtt

történjen meg, amelyből a felhasználó (nem a programozó) tudni fogja, hogy

mikor mit kell csinálnia, mit vár tőle a program, mit kaphat ő a programtól. A

felhasználóbarát szoftver fontos ismérve ez a fajta öndokumentáltság, azaz

az a képesség, hogy a program használata annak működése közben

érthetővé, megtanulhatóvá váljon. Ennek kialakítása nem implementációs

kérdés, hanem az ember-gép kapcsolat megértésének és tervezésének, a

szoftverergonómiának a része, amelyet az implementáció keretében

kódolunk hozzá az alkalmazáshoz. Ez többek között meghatározza, hogy az

adatokat és üzeneteket milyen formában, milyen adagokban közvetítse a

Page 130: PROGRAMOZÁSEz 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

130

program a felhasználó felé, illetve az adatokat hogyan kérje be a

felhasználótól.

Az input-output tevékenység másik velejárója a bemenő adatok

ellenőrzésének elvégzése, az úgynevezett „bolond-biztos” alkalmazás

készítése. Ez azt jelenti, hogy a felhasználó se hozzá nem értésből, se

rosszindulatból ne tudjon olyan adatokat megadni a programnak, amitől az

nem várt működést végez: például váratlan leáll (abortál) vagy végtelen

ciklusba esik. Azt, hogy milyen adatokat vár a program azt a specifikáció

megmutatja. Az input-output tevékenységért felelős kódba olyan

ellenőrzéseket kell beépíteni, amelyek eleve megakadályozzák a nem

kívánatos adatokkal való számításokat.

A beolvasásnál alkalmazott adatellenőrzésnek több szintje is van. A

legkülső szint a beolvasás szintje. Már ekkor is bekövetkezhet ugyanis hiba,

még mielőtt módunkban állna a beolvasott értéket megvizsgálni. Ahhoz

ugyanis, hogy egy adat értékét le tudjunk ellenőrizni, annak egy változóba

kell bekerülnie. Ha például egy számot tartalmazó változóba egy nem

számformájú adatot olvasunk be, akkor a program a legtöbb nyelven abortál.

Ezt el tudjuk úgy elkerülni, ha az adatot először egy általános, bármilyen

karakterláncot befogadni tudó változóba (mondjuk egy sztringbe) olvassuk

be, majd ezután megvizsgáljuk, hogy az a várt formájú karakterekből áll-e. Ha

nem, jelezzük a hibát a felhasználónak.

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

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

Page 131: PROGRAMOZÁSEz 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

131

előfeltétel vizsgálatát jelenti. Előbbire példa az, amikor egy bemenő változó a

feladat specifikációja szerint természetes szám, de az adott programozási

nyelv ezt csak egy integer (egész típusú) változóba képes beolvasni. Ilyenkor

ellenőrizni kell, hogy a beolvasott érték nem negatív-e, azaz tényleg

természetes szám-e. Ezt követően vizsgálni kell, hogy a beolvasott érték

kielégíti-e az előfeltételben vele szemben támasztott követelményeket.

Természetesen az ellenőrzés különböző szintjeit nem kell a kódban

szétválasztani, azok összevonhatók. Érdemes figyelni arra, ha több adat

bekérésére is sor kerül egymás után, akkor minden beolvasás után külön-

kü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.

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

1. Figyelmeztető üzenet után a program leállítása

2. Figyelmeztető üzenet után az adat ismételt bekérése

Page 132: PROGRAMOZÁSEz 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

132

nemcsak arról dönthet a felhasználó, hogy akarja-e folyatatni a futtatást,

hanem arról is, hogy azt milyen feltételek mellett akarja elvégezni, esetleg

egy több funkciós programnál kiválaszthatja, melyik funkciót akarja

kipróbálni. Itt egy úgynevezett menü implementálására van szükség. Ha az

egész alkalmazásra vonatkozik a menü, amely ráadásul végtelenítve van,

akkor mindig szerepeljen a menüpontok között a befejezést választó eset.

Még az eredetileg helyes absztrakt programot is félre lehet kódolni,

ezért utólag azt is ellenőrizni, tesztelni kell, de az input-output

tevékenységért felelős kód megfelelő működését csak így lehet ellenőrizni,

hiszen a tervezés nem tér ki ennek részletezésére.

Nyelvi elemek

A futtatási környezetek mindig biztosítanak valamilyen karakteres

üzemmódú soros elérésű konzolablakos input-output lehetőséget. Ennek

használata során a program különféle típusú értékeket vár a felhasználótól,

és küld a felhasználó felé. Bevitelnél az értékeket rendszerint a billentyűzet

segítségével gépelhetjük be úgy, hogy az értéket kifejező karaktersorozatot

adjuk meg. Beviteli tevékenységünket az <enter> billentyű megnyomásával

fejezzük be. A begépelt karakterek a konzolablakban is megjelennek az

aktuális pozíción. A kiírt értékek az őket leíró karaktersorozat formájában

jelennek meg a konzolablakban. A konzolablakba történő íráskor adott egy

aktuális sor és azon belül egy aktuális pozíció, amely azt mutatja, hogy a

kiírandó következő karaktert hol kell megjeleníteni. Egy karakter kiírása után

az aktuális pozíció a soron következő pozíció lesz, sor vége esetén ez a

következő sor eleje.

cin változó

cout kifejezés

<<

>>

Page 133: PROGRAMOZÁSEz 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

133

4-3. ábra. Szabványos input/output

Korszerű, több programozási nyelv által használt input-output

technika, az, amelyik a beolvasáshoz egy bemenő-, a kiíráshoz egy kimenő

szöveges adatfolyamot használ.

C++ nyelvben a cin egy a standard névtérben előre definiált olyan

speciális bemeneti szöveges adatfolyam, amely a szabványos bemeneten (ez

többnyire a billentyűzet) keletkező karaktersorozatot megkapja, és amelyből

aztán különböző értékeket tudunk kiolvasni. Ennek fordítottjaként a cout

adatfolyamba különféle értékeket tudunk beírni karaktersorozat formában,

amely végső soron a szabványos kimenetre (többnyire a konzol ablakba)

kerül.

A cin >> változó olvasó utasítás az adatfolyam karakterei alapján

próbálja a változó értékét előállítani. Ha az adatfolyam üres, akkor vár

legalább egy karakter, majd <enter> billentyű leütésére. A beolvasás az

adatfolyamban levő karakterek sorozatát dolgozza fel. Alapértelmezett

beállításkor először elhagyja a sorozat elején álló elválasztó jeleket (szóköz,

tabulátor jel, sorvége jel). Ezután sorban kiveszi azokat a karaktereket,

amelyekből a változó számára értéket tud előállítani. Például, ha a változó

egész típusú, akkor az első karakter lehet számjegy vagy egy előjel (+/-), az

azt követő karakterek pedig számjegyek. A kiolvasott karaktersorozat értékét

a változó kapja meg. Lehet, hogy ezek után már nem marad több karakter az

adatfolyamban, azaz az olvasás után az adatfolyam kiürül, de lehet, hogy

olyan karakter következik, amely már nem értelmezhető a változó

értékeként, akkor ez az adatfolyamban marad. Hibás működést okoz, ha az

olvasás nem tud értéket előállítani az adatfolyamban levő karakterekből,

például ha egy ’a’ betűt gépelünk be egy egész típusú változó beolvasásához.

Egy utasítással egyszerre, pontosabban egymás után több változónak is

értéket lehet adni: cin >> változó1 >> változó2.

Egy adat beolvasását célszerű összekapcsolni az adat ellenőrzésével.

Erre kétféle technikát említettünk. Hibás adat esetén figyelmeztetést írunk

ki, majd leállítjuk a program futását, vagy újra és újra megpróbálkozunk az

adatbekéréssel. Az előbbihez megfelelő nyelvi elem az elágazás, az utóbbihoz

Page 134: PROGRAMOZÁSEz 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

134

célszerű az úgynevezett hátul tesztelő ciklust használni, amennyiben ezt a

nyelv biztosítja. Ilyenkor ugyanis az olvasási és ellenőrzési folyamatot egyszer

mindenképpen el kell végezni, és csak az ellenőrzés után derül ki, hogy meg

kell-e ismételni ezeket a tevékenységeket, vagy tovább léphetünk.

Page 135: PROGRAMOZÁSEz 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

135

Adatfolyamok

Az adatfolyamok (stream) olyan objektumok, amelyek a hozzájuk

eljuttatott adatokat a beírásuk sorrendjében adják vissza, azaz sor (FIFO)

módjára működnek. Mivel az adatokat valamilyen kódsorozat formájában

tárolja, ezért az adatfolyam képes arra is, hogy ezt a sorozatot a beírás

logikájától eltérő szeletekre bontsa, és az egyes szeleteket kívánt értékké

alakítva adja vissza.

Leggyakrabban a szöveges adatfolyamokkal találkozhatunk, amelyek

között megkülönböztetünk bemeneti- illetve kimeneti adatfolyamot. A

bemeneti szöveges adatfolyamnak egy sztringet adunk inputként, amelynek

karaktereit a kiolvasás által meghatározott értékekké képes átalakítani és

visszaadni. Ezt úgy éri el, hogy a sztringet megfelelő szeletekre vágja, és az

egyes szeleteket alakítja át adott típusú értékké. A kimeneti szöveges

adatfolyamnak különböző értékeket lehet egymás után megadni, amelyeket

karakterláncokká alakít át és egyetlen sztringbe fűz össze, amelyet aztán

eredményként visszaad. Szöveges adatfolyamokkal kényelmesen végezhetők

el különféle átalakítások (konverziók) sztringek és egyéb értékek között.

A konzolos illetve szöveges állományból történő beolvasáshoz

bemeneti-, a kiíráshoz kimeneti speciális szövegfolyamokat használunk.

Ezeknek az adatfolyamoknak vagy a bemenete, vagy a kimenete valamilyen

perifériához (billentyűzet, képernyő, szöveges fájl) kapcsolódik. Bemenet

lehet például a billentyűzeten keletkező karaktersorozat, kimenet a

konzolablakra szánt karakterek sorozata, de lehet más, például a

merevlemezen tárolt, sorosan olvasható vagy írható állomány is. Ennek a

technikának az előnye az, hogy függetleníti a programot a perifériáktól,

egységessé teszi az input-output tevékenységet, nem nagyon kell

különbséget tenni például a billentyűzetről vagy egy szöveges állományból

való beolvasást végző kód között.

A hátul tesztelő ciklus a ciklusmagot egyszer mindenképpen

végrehajtja, és csak azt követően ellenőrzi a ciklus feltételt. Tulajdonképpen

egy olyan szekvencia, amelyiknek első tagja a ciklusmag, második tagja pedig

Page 136: PROGRAMOZÁSEz 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

136

egy szokásos (elöl tesztelő) ciklus, amelyiknek a ciklus magja azonos a

szekvencia első tagjával.

A C++ nyelvben a hátul tesztelős ciklust a do-while utasítással adhatjuk

meg. Fontos megemlíteni, hogy a while(ciklusfeltétel) részben a

zárójelezett kifejezés azt a logikai értéket állítja elő, amelynek igaz értéke

esetén a do és while közötti utasítást (vagy utasítás blokkot) meg kell

ismételni, hamis értéke esetén a ciklus utáni utasításra kerül a vezérlés. (Ez

éppen a fordítottja a Pascal nyelvbeli repeat-until utasítás működésének.)

ciklusmag do{

ciklusfeltétel ciklusmag

ciklusmag }while(ciklusfeltétel)

4-4. ábra. Hátul tesztelő ciklus

Ugyancsak hátul tesztelő ciklussal érdemes egy alkalmazás

„végtelenítését” kódolni. (Erre a 8. feladatnál már láttunk példát.) C++

nyelven ezt általában úgy érhetjük el, hogy a main függvény törzsét az utolsó

return 0 utasítás kivételével egy do-while utasításba ágyazzuk, és a

ciklusmag végén megkérdezzük a felhasználót, akarja-e megismételni a

program futását.

int main()

{

char ch;

do{

...

cout << "Folytatja? (I/N): "; cin >> ch;

Page 137: PROGRAMOZÁSEz 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

137

}while(ch != 'n' && ch != 'N');

return 0;

}

Ennek egy továbbfejlesztett változata az, amikor egy menü

segítségével vezéreljük a program futását. Erre a 10. feladat majd mutat

példát.

A fenti eseteken kívül a do-while ciklus alkalmazását programjainkban

nem ajánljuk.

A C++ nyelvben a kiírás a cout << kifejezés vagy cout <<

kifejezés1 << kifejezés2 utasításokkal végezhető. A kiírás a

konzolablak azon pontján kezdődik, ahol az előző kiírás befejeződött. Amikor

egy sor betelik, a kiírás automatikusan a következő sorban folytatódik. A

kiírás során szívesen élünk olyan lehetőségekkel, amellyekkel a kiírt értékeket

felhasználóbarát formában tudjuk elhelyezni a konzolablakban. Már egy

szóköz kiírásával jól szeparálhatóak a megjelenített adatok, de használhatunk

pozícionált kiírást. A cout << setw(10)<< kifejezés például a

következő 10 pozíción helyezi el a kifejezés értékét, attól függően balra vagy

jobbra tömörítve, hogy milyen típusú a kiírt érték, illetve hogyan állítottuk be

ezt az opciót előzőleg. (Erről illetve az ehhez hasonló opciókról a következő

bekezdésben írunk részletesen.) A tabulálást a cout << ’\t’ kiírás, a

soremelést a cout << endl vagy cout << ’\n’ biztosítja.

Végezetül ki kell térnünk a beolvasással kapcsolatos igen kellemetlen

jelenségre. Ha például egy egész számot akarunk beolvasni az eddig

alkalmazott

int n; cin >> n;

technikával, de nem számformájú karaktereket gépelünk be, akkor a

beolvasás elromlik. Az nem meglepő, hogy az n változó ilyenkor nem kap

értéket, de az már igen, hogy az ezt követő összes beolvasás sem fog már

helyesen működni.

Page 138: PROGRAMOZÁSEz 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

138

Ezt a jelenséget kétféleképpen tudjuk kezelni. Az egyik lehetőség az,

hogy egy beolvasást követően rákérdezünk arra, hogy a beolvasásnak milyen

az állapota. A cin.fail() függvény hamis értéket ad vissza, ha a beolvasás

„elromlott”. Ebben az esetben hibaüzenetet küldhetünk a felhasználónak,

újra bekérhetjük az adatot. Ahhoz azonban, hogy az ezt követő beolvasások

sikerüljenek, ki kell törölnünk a beolvasás „emlékeit”. Egyrészt visszaállítjuk a

beolvasás állapotát úgy, hogy a fail() függvény ne jelezzen továbbra is

hibát (cin.clear()), másrészt kiürítjük (getline(cin,tmp)) a beolvasás

során bevitt, de fel nem dolgozott karaktereket a bemeneti adatfolyamból.

Fontos, hogy ezt a kiürítést megelőzze a cin.clear() utasítás.

Manipulátorok, formátumjelzők C++ nyelvben

A formázott kimenetet a manipulátorok és a formátumjelző bitek

(flag) segítségével vezérelhetjük.

A formátumjelző bitek a kiírás formáját határozzák meg. Ilyen például

a scientific (lebegőpontos alak), fixed (fixpontos alak – alapértelmezett),

right, left (jobbra ill. balra tömörítés), dec, hex, oct (számrendszer, dec az

alapértelmezett), showpoint (tizedespont mindig látszódjon), showpos

(pozitív előjel is látszódjon), uppercase (csupa nagybetű), boolalpha (logikai

érték kiírásához). Ezeket a tulajdonságokat a setf() illetve unsetf()

függvények segítségével kapcsolhatjuk be illetve ki. E függvények

argumentumában | jellel elválasztva sorolhatjuk fel a szükséges

formátumjelző biteket. Némelyik tulajdonság alapértelmezett módon be van

kapcsolva. Minden formátumjelző bit elé az ios:: minősítést kell írni. Ha

például a számokat lebegőpontos alakban, balra tömörítve szeretnénk kiírni

úgy, hogy a tizedespont és az előjel minden esetben látszódjon, akkor ezt a cout.setf(ios::scientific|ios::showpoint|ios::showpos)

utasítással állíthatjuk be.

A manipulátorokat a << operátorral kell az adatfolyamra elküldeni, és a

következő kiírásra van hatással. Az endl kiírás egy soremelést eredményez,

a setw(int w)-vel a következő kiírás számára fenntartott szélességet,

pozíció számot adhatjuk meg. Egy valós szám törtjegyeinek számát például a

setprecision(int p), kitöltő karaktereket a setfill(char c) segítségével

Page 139: PROGRAMOZÁSEz 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

139

idézhetjük elő. A paraméterrel rendelkező manipulátorok eléréséhez az

iomanip csomagra van szükségünk. Számos formátumjelző bitként

megadható tulajdonság manipulátorként is be- illetve kikapcsolható

(showpos / noshowpos).

Ez a technika nemcsak a kiíráshoz használt I/O adatfolyamra

alkalmazható. Formátumjelző bitekkel és manipulátorokkal szabályozható a

beolvasás is. Természetesen más tulajdonságok állíthatóak be a

beolvasásnál, mint a kiírásnál. Csak beolvasásnál van értelme például a

vezető üres karakterek átugrására (cin >> ws >> …), csak kiírásnál a csupa

nagybetűs szövegként való megjelenítésre (cout << uppercase << …), a

logikai értékek kezelését előíró tulajdonság (boolalpha) viszont

beolvasásnál is, kiírásnál is használható.

int n; cin >> n;

if(cin.fail()){

cout << "Hiba!\n";

cin.clear();

}

string tmp; getline(cin,tmp);

Nemcsak a cin objektumra lehet ilyen hasznos függvényeket

(fail(), clear(), getline()) meghívni, hanem a cout objektumra is. A

cout objektumra hívható függvényekkel a kiírás formáját tudjuk

befolyásolni. A konkrét lehetőségeket az alábbi alkalmazásokban mutatjuk

majd be.

A másik megoldás az, ha az adatbevitelre szánt karaktereket először

mindig egy sztring típusú változóba olvassuk be, mert ez nem okozhat hibát,

majd ellenőrizzük, hogy a sztringbe bekerült karakterek megfelelnek-e,

például valóban egy egész számot írnak-e le. Ehhez felhasználjhatuk a C

Page 140: PROGRAMOZÁSEz 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

140

nyelvtől örökölt atoi() függvényt, amelyik helyes formátum mellett a

sztringből kiszámolt egész szám értékét, hibás formátum esetén a nullát adja

vissza. (Valós számok beolvasásánál a lebegőpontos formátumot a C nyelvből

örökölt atof() függvénnyel ellenőrizhetjük.) Így – ha csak nem a 0 karaktert

adjuk meg egész számként – a nulla visszatérési érték a hibás

adatformátumot jelzi. Sajnos az atoi() függvény csak régi (C stílusú)

karakterláncokra működik, ezért át kell alakítanunk a beolvasott sztringünket

ilyen lánccá a c_str() függvény segítségével.

string str; cin >> str;

int n = atoi(str.c_str());

if (0 == n && str != "0"){

cout << "Hiba!\n";

}

Page 141: PROGRAMOZÁSEz 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

141

Különféle karakterláncok

A C programozási nyelv a karakterláncokat olyan byte sorozatként

ábrázolja a memóriában, amelynek a végét egy speciális jel, a ’\0’ karakter

kódja jelez. Az ilyen karakterláncok kezelése nagy körültekintést igényel.

Nem is hinnénk milyen könnyen elhagyható vagy felülírható a legutolsó

speciális karakter, és hogy ennek milyen komoly következményei lehetnek.

Talán éppen ezért jelent meg egy igen gazdag beépített függvény készlet a

karakterláncok kezelésére.

Jóval biztonságosabb, de ugyanakkor korlátozottabb a Pascal

programozási nyelvben található karakterlánc ábrázolás, amely egy rögzített

méretű tömbben helyezi el a karakterláncot, külön tárolja annak hosszát,

amely természetesen nem lépheti át a maximális méretet.

A C++ nyelv a fenti két megoldást elegyítve bevezette a string típust,

amely alapvetően a Pascal-os megoldásra hasonlít (nincs a karakterlánc

méretének előre rögzített felső határa), de lehetőség van a C stílusú láncok

mintájára bevezetett függvények használatára is. A C++ nyelv átvett olyan

függvényeket is a C nyelvtől, amelyek argumentumának C stílusú

karakterláncot kell megadni. (Egyébként C stílusú karakterláncokat is

használhatunk C++ nyelven.) Ha ezeket a függvényeket (például a

karakterláncot egész számmá konvertáló atoi()-t) szeretnénk használni egy

C++ stílusú karakterláncra, azaz sztringre, akkor át kell tudnunk alakítani azt C

stílusú karakterlánccá. Erre szolgál a c_str() függvény.

Valójában ez egy körülményes megoldás arra a problémára, hogy a

C++ nyelv nem jár el következetesen a karakterláncok használatakor.

Page 142: PROGRAMOZÁSEz 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

142

9. Feladat: Duna vízállása

Vízügyi szakemberek egy adott időszakban rögzítették a Duna Budapesten

mért vízállásait. Készítsünk hisztogramot a mért adatokról, miután

ellenőriztük az adatok helyességét.

Ez a feladat különleges abból a szempontból, hogy tényleges számítást,

tehát az előző fejezetek feladatainak megoldásainál előállított absztrakt

megoldó programot nem igényel. A feladat ugyanakkor határozott

kívánságot fogalmaz meg a kiírással szemben. Ezért az absztrakt program itt a

korábbiaktól eltérő szerepkörben jelenik meg, a kiírás tervét hordozza.

Tipikus határesettel van tehát dolgunk. Eddig a kiírás részekkel kizárólag az

implementációban foglalkoztunk, de most ezt is részletesen specifikálni kell,

részben absztrakt programot kell hozzá készíteni.

Specifikáció

A feladatnak csak bemenő változója van, ennek megfelelően célfeltétele

sincs.

A = ( v : ℤ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

Page 143: PROGRAMOZÁSEz 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

143

rajzoljuk ki, a hasábokat pedig megfelelő számú egymás után írt ’*’

karakterrel helyettesítsük. Írjuk ki a hasáb mellé, hogy az hányadik méréshez

tartozik, és jelenítsük meg a mért értéket is.

Absztrakt program

A hisztogram arányos megjelenítéséhez szükség van a megjelenítendő

legnagyobb értékre.

A = ( v : ℤn, max : ℤ )

Ef = ( v=v’ n>0 )

Uf = ( v=v’ max = maxn

1i

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

Page 144: PROGRAMOZÁSEz 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

144

tetszőleges v[i] értékéhez tartozó hasábban (v[i]/max)*m egészrésze számú

csillagot kell majd kiírnunk.

i = 1..n

i kiírása

j = 1.. (v[i]/max)*m

’*’ kiírása

v[i] kiírása

Implementálás

Beolvasás

A vector<int> segítségével definiált tömb feltöltése lényegében

megegyezik az előző fejezetben látottakkal, de most „bolond-biztossá”

tesszük az egész számok beolvasását és hibás adatbevitel esetén egy

hibaüzenet kiírása után azonnal leállítjuk a programot. Ezt alkalmazzuk a

tömb elemszámának beolvasásánál és a tömb elemeinek beolvasásánál is.

cout << "Adja meg a tömb hosszát (1 <= n )";

int n;

cin >> n;

if(cin.fail() || n<=0){

cout << "Hiba!\n";

return 1;

}

Page 145: PROGRAMOZÁSEz 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

145

vector<int> v(n);

cout << "Adja meg a tomb elemeit!\n";

for(int i=0; i<n; ++i){

cout << i + 1 << ". elem: ";

cin >> v[i];

if(cin.fail() || v[i]<0){

cout << "Hiba!\n";

return 1;

}

}

Kiírás

Klasszikus értelemben vett számításrész most nincsen, viszont a kiírás módját

az absztrakt program leírja. Ez egyrészt egy (az előző fejezetben már látott)

maximum kiválasztásból áll, és egy formázott kiírást végző részből.

int ind = 0, max = v[0];

for(int i=1; i<n; ++i){

if (v[i]>max){

ind = i; max = v[i];}

}

int m = 30;

Page 146: PROGRAMOZÁSEz 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

146

cout << endl;

for(int i=0; i<n; i++){

cout << setw(5) << i+1 << " ";

for(int j=0; j<(v[i]/max)*m; i++){

cout << setw(2) << "*";

}

cout << v[i] << endl;

}

A soron következő formázott kiíráshoz használtuk a setw(int w)

manipulátort, amely azt állítja be, hogy a soron következő kiírás w pozíción

(helyen, szélességben) történjen. Szám kiírása esetén alapértelmezés szerint

a megadott mező jobbszéléhez illesztve fog a szám értéke megjelenni,

karakter, karakterlánc esetén a balszéléhez illesztve. Ennek a manipulátornak

a használatához szükség van az iomanip könyvtárra. (#include

<iomanip>). A manipulátorokat a << operátorral kell a kimeneti

adatfolyamra elküldeni. A setw()-hez hasonló manipulátorral már korábban

is találkoztunk: ilyen volt a soremelésre használt endl, csak ahhoz még nem

kellett az iomanip könyvtár.

Tesztelés

Ez a program elsősorban fehérdoboz módszerrel tesztelhető. Négy szakaszra

bontható: a tömb definiálása, tömb feltöltése, tömb maximális elemének

kiválasztása és a kiírás. Az első három fázis tesztelése a 6. feladatáéval

azonos módon történik.

A kiírás rész tesztelése az eredmény megjelenési formájának

ellenőrzéséből áll. Különböző hosszú és értékű tömb segítségével

próbálhatjuk ki, hogy a kiírás egymásba ágyazott ciklusai megfelelően

működnek-e.

1. Külső ciklus magja egyszer sem fut le.

Page 147: PROGRAMOZÁSEz 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

147

2. Külső ciklus magja egyszer/többször fut le, a belső ciklus magja

egyszer sem/egyszer/többször.

3. A tömbnek egy kiugróan magas értéke van.

Page 148: PROGRAMOZÁSEz 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

148

Teljes program

#include <iostream>

#include <iomanip>

#include <vector>

#include <string>

using namespace std;

int main()

{

// Tömb definiálása

cout << "Adja meg a tömb hosszát (1 <= n )";

int n; cin >> n;

if(cin.fail() || n<=0){

cout << "Hiba!\n";

return 1;

}

vector<int> v(n);

cout << "Adja meg a tomb elemeit!\n";

for(int i=0; i<n; ++i){

cout << i + 1 << ". elem: ";

cin >> v[i];

if(cin.fail() || v[i]<0){

cout << "Hiba!\n";

Page 149: PROGRAMOZÁSEz 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

149

return 1;

}

}

// Maximumkeresés

int ind = 0, max = v[0];

for(int i=1; i<n; ++i){

if (v[i]>max){

ind = i; max = v[i];

}

}

// Hisztogram rajzolása

int m = 30;

cout << endl;

for(int i=0; i<n; i++){

cout << setw(5) << i+1;

for(int j=0; j<(v[i]/max)*m; i++){

cout << " *";

}

cout << v[i] << endl;

}

}

Page 150: PROGRAMOZÁSEz 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

150

10. Feladat: Alsóháromszög-mátrix

Két, valós számokat tartalmazó alsóháromszög mátrixot (ez olyan mátrix,

amelynek a főátlója feletti elemei mind nullák) szorozzunk össze! Miután a

mátrixokat beolvastuk a billentyűzetről és elvégeztük az összeszorzásukat,

egy menüből válasszuk ki, hogy milyen formában akarjuk a mátrixot

megjeleníteni: kiírjuk-e a főátló feletti értékeket vagy csak a helyüket

jelezzük; az értékek lebegőpontos formában vagy fixpontos formában

jelenjen meg, illetve hogy a kiírás pontossága milyen legyen.

Specifikáció

A feladat két alsóháromszög mátrix összeszorzása. Az általános mátrixszorzás

képlete: i[1..n]:j[1..n]: c[i,j] = ∑ [ ] [ ] ). Alsóháromszög

mátrixok esetén ezt a képletet egyszerűsíteni lehet. Egyrészt az eredmény

mátrixnak csak az alsóháromszög részét kell kiszámolnunk, mivel

alsóháromszög mátrixok szorzata is alsóháromszög alakú. Másrészt a

számolás során a biztosan nulla értékű szorzatokat (amikor valamelyik

mátrixnak a jobb-felső területéről származó értékkel szorzunk) felesleges

kiszámítanunk: i[1..n]:j[1..i]: c[i,j] = ∑ [ ] [ ] )

Érdemes azt is észrevenni, hogy egy alsóháromszög mátrix főátló

feletti nulla értékeit nem kell külön eltárolni. Az alsóháromszög részbe eső

elemek elférnek egy n(n+1)/2 elemű egydimenziós tömbben. Ha

sorfolytonosan helyezzük ebbe a tömbbe a mátrix alsóháromszög részének

elemeit, akkor az i-dik sor j-dik eleme a tömb i(i–1)/2+j -dik eleme lesz. Az

állapottérbe az a, b, c : ℝn×n mátrixok helyett a, b, c : ℝ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 : ℝ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]) ) )

Page 151: PROGRAMOZÁSEz 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

151

Absztrakt program

i = 1 .. n

j = 1 .. i

c[i(i–1)/2+j] := 0.0

k = j .. i

c[i(i–1)/2+j] := c[i(i–1)/2+j] +

a[i(i–1)/2+k] * b[k(k–1)/2+j]

Implementálás

Ügyeljünk arra, hogy a C++ nyelv 0-tól kezdődően indexeli a tömböket, ezért

az absztrakt programban használt tömbindexelés mindenhol helyett eggyel

csökkentett indexet használjunk. Például a mátrix i,j-edik elemére történő

hivatkozáskor az i(i–1)/2+j helyett i(i–1)/2+j–1-et.

Beolvasás

Miután beolvastuk a mátrixok közös méretét (n), egy mátrix beolvasása,

amely valójában egy egydimenziós tömb feltöltése, az alábbi szerint alakul:

int n;

cin >> n;

vector<double> a(n*(n+1)/2);

for(int i=1; i<=n; ++i){

for(int j=1; j<=i; ++j){

cout << "a[" << i << "," << j << "]= ";

Page 152: PROGRAMOZÁSEz 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

152

cin >> a[(i-1)*i/2 + j - 1];

}

}

A végleges változatban azonban minden beolvasás köré építsünk egy

hibaellenőrző hátul-tesztelős ciklust, amely mindaddig újra és újra adatot

kér, amíg nem adjuk meg azt helyesen.

Számolás

A szorzás az absztrakt program hű másolata. Felhasználjuk azt, hogy a

vector típusú adat létrehozásakor meg lehet adni egy a vektor elemeit

kezdetben kitöltő értéket.

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

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.

Page 153: PROGRAMOZÁSEz 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

153

Az egyéb beállítási lehetőségeket a fejezet végén található C++ kislexikonban

olvashatjuk.

cout.setf(ios::fixed|ios::left|ios::showpoint);

cout << setprecision(2);

for(int i=1; i<=n; ++i){

for(int j=1; j<=n; ++j){

if(i>=j) cout << setw(10) << c[(i-1)*i/2+j-1];

else cout << setw(10) << 0.0;

}

cout << endl;

}

A második esetben a főátló feletti elemeknek csak a helye látszik, a

nulla értékek nem, a többi szám pedig lebegőpontos formában 1 tizedesjegy

pontossággal, mindig mutatva az előjelet, tíz pozíción jobbra igazítva.

cout.setf(ios::scientific|ios::showpos);

cout << setprecision(1);

for(int i=1; i<=n; ++i){

for(int j=1; j<=i; ++j){

cout << setw(10) << c[(i-1)*i/2+j-1];

}

cout << endl;

}

Page 154: PROGRAMOZÁSEz 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

154

A fenti két programrészt az alábbi kódba ágyazzuk.

char ch;

do{

cout << "Kiírás módja:\n";

cout << "1 - Főátló feletti elemekkel, "

<< "fixpontosan, balra igazítva, "

<< "2 tizedesjegy pontossággal\n";

cout << "2 - Főátló feletti elemek nélkül, "

<< "lebegőpontosan, előjelesen, " <<

<< "1 tizedesjegy pontossággal\n";

cout << "Válasszon: ";

int k; cin >> k;

switch(k){

case 1: // első fajta kiírás

break;

case 2: // második fajta kiírás

break;

default:;

}

cout << "Folytatja? (I/N): "; cin >> ch;

Page 155: PROGRAMOZÁSEz 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

155

}while(ch != 'n' && ch != 'N');

Ez egy „végtelenített” működést valósít meg. Az ismétlődő rész egy

kételemű menüt ajánl fel, és a megfelelő menüpont kiválasztásával a kiírás

formáját lehet meghatározni. Ehhez a C++ nyelv speciális elágazó utasítását,

a switch() utasítást használjuk. Az utasítással egy kifejezést adunk meg (itt

ez egyszerűen a k), amelynek lehetséges értékeit sorolják fel az esetek

(case). Minden esethez tartozó program végén a break utasítás áll,

különben a vezérlés a következő eset programjára kerül, nem pedig a switch

után. Ha a kifejezés olyan értéket vesz fel, amelyik nem szerepel esetként,

akkor a switch utasítás nem csinál semmit. Ezt emeli ki az üres utasítású

default ág, amelyre ugyan nincs szükség, de explicit módon való kiírása a

biztonságos kód kialakítását támogatja.

Tesztelés

A feladat szempontjából készített fekete doboz tesztesetek:

Érvényes tesztesetek:

1. Null-mátrixszal való (jobbról, balról) szorzás (Eredmény: null-

má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:

Page 156: PROGRAMOZÁSEz 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

156

1. Hibás méret (<=0) beírása.

A programkód alapján készített tesztesetek:

1. Mátrix méretének beolvasása és ellenőrzése: amikor a ciklus egyszer

fut le (jó adat), többször fut le (először rossz adat, a végén jó).

2. Mátrixok feltöltését és a mátrixok szorzását végző ciklusok

ellenőrzése (fekete doboz tesztestek)

3. A kiírás tesztelése (kétféle kiírás ciklikusan ismételve)

Page 157: PROGRAMOZÁSEz 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

157

Teljes program

#include <iostream>

#include <iomanip>

#include <vector>

#include <string>

using namespace std;

int main()

{

// Mátrix méretének beolvasása

int n;

bool error;

do{

cout << "Adja meg a mátrixok méretét: ";

cin >> n;

if(error = cin.fail() || n<1){

cout << "Pozitív egész szám legyen!\n";

cin.clear();

}

string tmp; getline(cin,tmp);

}while(error);

Page 158: PROGRAMOZÁSEz 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

158

// Első mátrix beolvasása

cout << "Első mátrix:\n";

vector<double> a(n*(n+1)/2);

for(int i=1; i<=n; ++i){

for(int j=1; j<=i; ++j){

do{

cout << "a[" << i << "," << j << "]= ";

cin >> a[(i-1)*i/2+j-1];

if(error = cin.fail()){

cout << "Valós szám legyen!\n";

cin.clear();

}

string tmp; getline(cin,tmp);

}while(error);

}

}

// Második mátrix beolvasása

Page 159: PROGRAMOZÁSEz 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

159

cout << "Második mátrix:\n";

vector<double> b(n*(n+1)/2);

for(int i=1; i<=n; ++i){

for(int j=1; j<=i; ++j){

do{

cout << "b[" << i << "," << j << "]= ";

cin >> b[(i-1)*i/2+j-1];

if(error = cin.fail()){

cout << "Valós szám legyen!\n";

cin.clear();

}

string tmp; getline(cin,tmp);

}while(error);

}

}

// Mátrix-szorzás

vector<double> c(n*(n+1)/2, 0.0);

for(int i=1; i<=n; ++i)

for(int j=1; j<=i; ++j)

for(int k = j; k<=i; ++k)

c[(i-1)*i/2+j-1]+=

a[(i-1)*i/2+k-1]*b[(k-1)*k/2+j-1];

//Kiírás kétféleképpen

char ch;

Page 160: PROGRAMOZÁSEz 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

160

do{

cout << "Kiírás módja:\n";

cout << "1 - Főátló feletti elemekkel, "

<< "fixpontosan, balra igazítva, "

<< "2 tizedesjegy pontossággal\n";

cout << "2 - Főátló feletti elemek nélkül, "

<< "lebegőpontosan, előjelesen, " <<

<< "1 tizedesjegy pontossággal\n";

cout << "Válasszon: ";

int k;

do{

cout << "Adja meg a mátrixok méretét: ";

cin >> k;

if(error = cin.fail() || n<1){

cout << "Pozitív egész szám legyen!\n";

cin.clear();

}

Page 161: PROGRAMOZÁSEz 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

161

string tmp; getline(cin,tmp);

}while(error);

switch(k){

case 1: cout.setf(ios::fixed|

ios::left|ios::showpoint);

cout << setprecision(2);

for(int i=1; i<=n; ++i){

for(int j=1; j<=n; ++j){

if(i>=j)

cout << setw(10)

<< c[(i-1)*i/2+j-1];

else

cout << setw(10) << 0.0;

}

cout << endl;

}

break;

case 2: cout.setf(ios::scientific|

ios::showpos);

cout << setprecision(1);

for(int i=1; i<=n; ++i){

for(int j=1; j<=i; ++j){

cout << setw(10)

Page 162: PROGRAMOZÁSEz 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

162

<< c[(i-1)*i/2+j-1];

}

cout << endl;

}

break;

default:;

}

cout << "Folytatja? (I/N): "; cin >> ch;

}while(ch != 'n' && ch != 'N');

return 0;

}

Page 163: PROGRAMOZÁSEz 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

163

C++ kislexikon

standard

beolvasás

cin >> változó;

cin >> változó1 >> változó2;

Standard kiírás 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();

}

Page 164: PROGRAMOZÁSEz 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

164

string tmp; getline(cin,str);

}while(error);

végtelenített

futtatás

char ch;

do{

cout << "Folytatja? (I/N): ";

cin >> ch;

}while(ch != 'n' && ch != 'N');

menü char n;

do{

cout << "Menü pontok jelentése …";

cout << "Válassz: ";cin >> n;

switch(n){

case 1: … ; break;

case 2: … ; break;

default: … ;

}

}while(n != 0);

szerkesztett

input-output

#include <iomanip>

Page 165: PROGRAMOZÁSEz 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

165

cin.setf(ios::flag)

cin.unsetf(ios:: flag)

cout.setf(ios:: flag)

cout.unsetf(ios:: flag1|ios:: flag2)

cout << manipulator

formátum jelzők

(flag)

scientific, fixed lebegőpontos ill.

fixpontos alak

right, left, jobbra ill. balra

tömörítés

dec, hex, oct, megjelenítés

számrendszere

showpoint, showpos tizedespont ill. előjel

mindig látszódjon

skipws elválasztó jelek

átlépése olvasáskor

boolalpha logikai érték kiírásához

uppercase csupa nagybetű

manipulátorok

(manipulator)

setw(int w)

width(int w)

mezőszélesség

megadása

setprecision(int p)

precision(int p)

számábrázolás

pontossága

Page 166: PROGRAMOZÁSEz 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

166

setfill(char c) kitöltő karakter

definiálása

endl sorvége

Page 167: PROGRAMOZÁSEz 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

167

5. Szöveges állományok

A szöveges állományok (szöveges fájlok) az adatokat karakteres formában

háttértárolókon tárolják. Tartalma egy szöveg, amely lényegében egyetlen,

többnyire igen hosszú, a felhasználó által könnyen elolvasható karakterlánc.

A karakterek között lehetnek olyanok is, amelyek a szöveg megjelenítését

befolyásolják. Már egyszerű szövegszerkesztők is képesek egy ilyen szöveget

úgy megjeleníteni, hogy a szöveg ilyen speciális karaktereit a megjelenítés

formáját meghatározó jeleknek (tabulátor, sorvége) tekintik.

A szöveges állományokat kétféle célból fogjuk használni: olvasásra

illetve írásra. Ehhez az első esetben egy „vedd a következő adatot” jellegű

olvasó műveletre, a második esetben „tedd az eddigi adatok után” írás

műveletre lesz szükségünk.

Implementációs stratégia

Egy alkalmazás implementálásakor sokszor feltett kérdés az, hogy a bemenő

adatokat billentyűzetről vagy szöveges állományból vegyük-e, illetve az

eredményt terminálra vagy szöveges állományba írjuk-e. Egyszerűbb,

kevesebb előkészítést igényel a konzolablakos input-output, ráadásul egy

hibásan beírt adatot azonnal, a program futása közben lehet korrigálni, ha az

alkalmazás ezt lehetővé teszi. Kevés számú bemenő adat esetén ezt a

megoldást ajánljuk. Ha viszont sok bemenő adatra van szükség (például egy

mátrix elemeit kell megadni), akkor érdemes azokat egy szöveges

állományba előre beírni. Az alkalmazás tesztelésekor ez mindenképpen

kifizetődő, de a felhasználó számára is áttekinthetőbb, ha az adatokat

előzetesen egy szövegszerkesztőben láthatja. Sajnos hibás adat esetén a

pánikszerű leállás az egyetlen biztos módszer, hiszen futás közben már nem

lehet a szöveges állományon módosítani. Az eredmény szöveges állományba

történő írásának az a kézzel fogható előnye, hogy az a program befejeződése

után is olvasható lesz az eredmény.

Egy szöveges állomány szabadon szerkeszthető, ami különösen nagy

felelősséget ró az őt használó programra. A „bolond biztos” működés

biztosítása általában igen nehéz, ezért formai megkötéseket szoktak

Page 168: PROGRAMOZÁSEz 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

168

megfogalmazni a szöveges állományra nézve, amelynek a betartása a

felhasználó felelőssége: ha az állomány nem megfelelő formájú, akkor a

programnak nem kell jól működnie.

Sokszor fordul elő, hogy a szöveges állományban elhelyezett azonos

típusú értékeket egy tömbbe kell bemásolni. A tevékenység megkezdése

előtt létre kell hozni a tömböt, ehhez pedig jó tudni, hogy hány érték

beolvasására kerül majd sor, azaz mekkora lesz a tömb mérete.

Ha ez a méret már a fordítási időben ismert állandó (konstans), akkor

könnyű dolgunk van: definiálunk egy ilyen méretű tömböt (erre szinte

mindegyik programozási nyelven van lehetőség), majd (egy for ciklussal)

feltöltjük az elemeit az állományból olvasott értékekkel, feltéve, hogy az

állomány hátralevő részéből megfelelő számú értéket ki lehet olvasni. Ekkor

már csak az a kérdés, hogy a szöveges állomány tényleg tartalmazza a

megadott számú adatot, és kell-e hibajelzést adni, ha nem.

Sokszor olyan programot várnak tőlünk, amelyik számára csak futási

időben derül ki a létrehozandó tömb mérete, de még a tömbbe szánt elemek

beolvasása előtt. Ilyenkor a futás közben kell létrehoznunk a megadott

méretű tömböt, amelyet utána az előbb ismertetett módon (egy for ciklussal)

tölthetünk fel.

Ha a választott programozási nyelv nem teszi lehetővé a futás közben

történő tömbméret megadását (például Pascal nyelv), akkor egy kellően

nagyméretű tömböt kell definiálnunk, futás közben beolvassuk a tömb

tényleges méretét (remélve, hogy ez nem nagyobb, mint a maximális méret),

ezt a méretet külön eltároljuk és egy for ciklussal feltöltjük a tömböt. Ennek a

megoldásnak az a hátránya, hogy esetenként túl pazarló, mert túl nagy

tömböt hozunk létre felesleges elemekkel, vagy éppen fordítva, a rossz

előkalkuláció miatt nem elegendő méretű tömböt foglalunk le.

Bonyolultabb a helyzet, ha a szöveges állomány csak a tömbbe szánt

értékeket sorolja fel, és semmilyen formában nem áll rendelkezésünkre előre

ezek száma. Ilyenkor több lehetőség közül választhatunk.

Az egyik lehetőség az, amit már az előbb ismertettünk. Lefoglalunk egy

kellően nagy méretű tömböt, majd addig olvassuk az újabb és újabb

értékeket az állományból (egy while ciklussal), amíg vagy egy speciális, az

elemek végét jelző értékhez vagy az állomány végére nem érünk. (Az

Page 169: PROGRAMOZÁSEz 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

169

állomány végét is egy speciális karakter jelzi, de a beolvasást végző nyelvi

elemek gyakran elfedik ezt, és más módon adják tudtunkra azt, hogy elértünk

az állomány végére.)

Hatékonyabb az a megoldás, amelyik egy fokozatosan nyújtózkodó

tömböt alkalmaz. Ennek mérete az állományból történő (while ciklusos)

olvasás során lépésről-lépésre nő, így a beolvasás végén éppen a kívánt

méretű tömbbel fogunk rendelkezni. Az azonban külön vizsgálandó, hogy a

választott programozási nyelv rendelkezik-e ilyen lehetőséggel (mint például

a C++ nyelv vector<> típusa vagy a C# List típusa), vagy ha nem, megéri-e

megteremteni a lehetőségét egy ilyen tömbnek.

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

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.

Page 170: PROGRAMOZÁSEz 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

170

érték van benne (ehhez az előbb javasolt while ciklus kell), ezt követően

létrehozzuk a szükséges méretű tömböt, és egy második menetben

megfelelő számú értéket kiolvasva az állományból feltöltjük a tömböt (a már

korábban említett for ciklussal).

Az adott jelig vagy az állomány végéig tartó while ciklussal végrehajtott

olvasást többnyire előreolvasási technikával valósítjuk meg. Ez a

megállapítás a legtöbb nyelvre, köztük a C-szerű nyelvekre (C++-ra is) is igaz.

Ennek a feldolgozásnak az a lényege, hogy először megkíséreljük a soron

következő érték beolvasását, majd csak ezt követően vizsgáljuk meg, hogy

sikerült-e az olvasás (nem értünk-e az állomány végére, nem olvastunk-e

speciális jelet, amely az adatok végét jelzi), és csak ezután dolgozzuk fel a

beolvasott értéket. Egy ilyen feldolgozásban az olvasó utasítás a ciklusfeltétel

ellenőrzése előtt kell, hogy megjelenjen. Az alábbi algoritmus-séma mutatja

be az előreolvasási technikát.

következő érték olvasása

while( nincs fájlvége vagy

a beolvasott érték nem speciális jel){

beolvasott érték feldolgozása

következő érték olvasása

}

Ebben az olvasó utasítás két helyen is szerepel: a ciklus előtt és a ciklus

mag végén, mert ez biztosítja, hogy a ciklusfeltétel kiértékelése közvetlenül

az olvasás után történjen. A bemutatott technikának az előnye az, hogy a

beolvasandó adatok végét ugyanúgy kell vizsgálni akkor is, ha azt az állomány

vége jelzi, vagy ha egy speciális jel. (A C++ nyelv lehetőséget ad arra, hogy a

ciklusfeltétel helyén szerepeljen az olvasó utasítás, azaz formailag csak

egyszer.)

Nyelvi elemek

Page 171: PROGRAMOZÁSEz 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

171

A szöveges állományok kezelése hasonlít a konzolos input-outputhoz. Ez

különösen így van azoknál a programozási nyelveknél, ahol az adatok be- és

kivitele adatfolyam-kezeléssel történik. Egy alkalmazás számára végül is

mindegy, hogy egy bemenő adatfolyam a billentyűzetről vagy egy szöveges

állományból származó karakterláncot fogad-e, az adatok adatfolyamból

történő kiolvasására ez nincs hatással. Ugyanez mondható el a kiírásról is.

Minden szöveges állománynak van egy úgynevezett fizikai neve

(útvonal+név+kiterjesztés). Ez az, amivel a háttértárolón a szöveges

állományt azonosítani lehet. A szöveges állományokra egy programban egy

belső névvel szoktak hivatkozni: ez az állomány logikai neve. Adatfolyamok

használatakor a logikai név valójában nem az állománynak, hanem annak az

állománnyal összekapcsolt adatfolyam objektumnak a neve, amelyen

keresztül tudunk az állományból olvasni vagy az állományba írni. Ennek

ellenére cseppet sem zavaró, ha az adatfolyam objektumra úgy tekintünk,

mint magára a szöveges állományra, a fájlra. Ez mutatkozik meg az alább

bemutatott fogalmak elnevezésében (fájlnyitás, fájlbezárás, stb.) is.

Amikor az alkalmazásban hozzákötjük a fizikai névhez a logikai nevet,

megnyitjuk a szöveges állományt. Szöveges állományokat többnyire vagy

kizárólag olvasásra, vagy kizárólag írásra használunk. A fájlnyitáskor meg kell

vizsgálni, hogy a megnevezett szöveges állomány tényleg létezik-e, ha nem,

hibajelzést kell adni, és vagy bekérni újra az állomány fizikai nevét (hátha

rosszul adtuk meg), vagy le kell állítani az alkalmazást.

Ha már nincs szükség a szöveges állományra, akkor le kell zárni annak

használatát. A fájlbezárás elhagyása az írásra megnyitott fájl esetén

adatvesztéshez vezethet. Egy szöveges állományba történő írás során ugyanis

nem ugyanabban az ütemben kerülnek a karakterek az állományba, mint

ahogy az alkalmazás utasításai ezt előírják. A háttértárolóra vonatkozó

műveletek futásai ideje ugyanis nagyságrenddel lassúbb, mint a memória

műveleteké, ezért a futtatási környezetet biztosító operációs rendszer

összevárja a kiírandó adatok egy csoportját és azokat egyszerre, egy

blokkban továbbítja a háttértárolónak. Lezáráskor az utolsó, még ki nem írt

blokk adatai is kiíródnak az állományba.

Az

ifstream ifile

Page 172: PROGRAMOZÁSEz 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

172

ofstream ofile

definiál egy úgynevezett bementi illetve egy kimeneti adatfolyam

objektumot (ezeknek a neve itt ifile és ofile). A fájlhasználathoz

szükséges nyelvi elemeket az fstream könyvtár definiálja, ezért a

programunk elején szükség lesz az #include <fstream> sorra.

Az adatfolyamokhoz tartozó fájlok neveit az

ifile.open(fnev.c_str())

ofile.open(fnev.c_str())

utasításokkal adhatjuk meg. Ezek a bemeneti adatfolyamhoz tartozó fájt meg

is nyitják, a kimeneti adatfolyamhoz tartozót pedig létre is hozzák.

Az adatfolyamhoz tartozó fájl nevét archaikus (C stílusú) karakterlánc

formában kell megadnunk. Ehhez elég annyit tudni, hogy egy string típusú

változóban (legyen a neve: str) tárolt (korszerű, C++ stílusú) sztringnek az

str.c_str() adja meg az archaikus alakját.

Az adatfolyam definíciója és a hozzátartozó fájl megnyitása

összevonható egy lépésbe.

ifstream ifile(fnev.c_str())

ofstream ofile(fnev.c_str())

A fájlok lezárására automatikusan sor kerül akkor, amikor az

adatfolyam élettartama lejár (például a vezérlés az adatfolyam deklarációját

tartalmazó blokk végéhez ér). A lezárás azonban explicit módon is

kikényszeríthető:

ifile.close()

ofile.close()

Page 173: PROGRAMOZÁSEz 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

173

Egy fájl megnyitásakor különféle hibák történhetnek. A leggyakoribb

az, hogy a megnevezett állományt nem találjuk meg, mert vagy elfelejtettük

létrehozni, vagy nem abban a könyvtárban van, ahol keressük. Az esetleges

hibára a fail() függvénnyel kérdezhetünk rá.

ifstream ifile;

if(ifile.fail()){

cout << "Hiányzik az állomány\n";

return 1;

}

Ciklusba is szervezhetjük a fájlnyitást, csak ilyenkor a fájlt azonosító

adatfolyamot a clear() művelettel újra inicializálni kell, mielőtt újra

megpróbáljuk megnyitni. Erre példát a feladatok megoldó kódjaiban

mutatunk majd.

Az olvasás az ifile >> változó, az írás az ofile << kifejezés

utasítással történik, amely igencsak hasonlít a konzolos input-outputhoz.

Akárcsak ott, most is szöveges adatfolyam objektumokkal van dolgunk,

amelyekkel a szöveges állomány tartalmát, egy karakterláncot tudunk

megfelelően szeletelve beolvasni, vagy a kiírásnál összeállítani.

Fájlvége kezelése a programozási nyelvekben

Amikor egy szöveges állományból olvasunk egy C++ nyelv nyújtotta

adatfolyam segítségével, és az állomány karaktereit már sorban kiolvastuk,

akkor – tehát egy sikertelen olvasás után – beállítódik egy speciális jelzőbit

(eof flag), amely ezután azt mutatja, hogy az olvasás során az állomány

végéhez értünk. Fontos tudni, hogy az állomány megnyitása nem inicializálja

az eof flag értékét, azt csak az első olvasás teszi meg: sikeres olvasás esetén

0, sikertelen olvasás esetén 1. Az eof flag értékét közvetett módon az

olvasáshoz használt adatfolyam eof() függvényével kérdezhetjük le: igaz

érték ad vissza, ha a flag értéke 1, különben hamisat.

Page 174: PROGRAMOZÁSEz 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

174

Ez a viselkedés az oka annak, hogy a C++ nyelven a már említett előre

olvasási technikát kell alkalmazni. Előbb kell olvasni, és utána kiértékelni az

olvasás eredményét. Az ismertebb programozási nyelvek ezt a megoldást

követik.

Ettől lényegesen eltér a Pascal programozási nyelv fájlkezelése. Ez a

nyelv egy olyan fájlolvasási műveletet ajánl fel, amely sikertelen olvasási

kísérlet esetén abortál. Ezért minden olvasás előtt meg kell vizsgálni, hogy

elértük-e a fájl végét. Ehhez a fájlkezelés egy speciális mutatót használ, amely

a szöveges állomány soron következő, még ki nem olvasott karakterére

mutat. Ha ez a karakter a sorvége jel, akkor elértük a fájl végét. Ezt az

eseményt itt is egy eof() függvénnyel kérdezhetjük le. De míg más

nyelvekben az eof() függvény akkor ad vissza igaz értéket, amikor már

kiolvastuk a fájlvége jelet, addig itt akkor, amikor elértük azt, de még nem

olvastuk ki.

A fájlból történő olvasás esetén a szöveges állomány aktuális, a már

feldolgozott karakterek utáni első karakterétől kezdve próbál beolvasni egy a

változó típusának megfelelő értéket. Alapértelmezett beállítás mellett átlépi

az elválasztó jeleket (szóköz, tabulátor jel, sorvége jel), majd sorban olvassa

azokat a karaktereket, amelyekből a változó számára érték állítható elő.

Például, ha a változó egész típusú, akkor az első ilyen karakter lehet számjegy

vagy egy előjel (+/-), az azt követő karakterek pedig számjegyek. Az olvasás

addig tart, amíg nem következik egy nem-számjegy karakter. Legjobb, ha az

értéket hordozó karaktersorozatot az állományban egy elválasztójellel zárjuk.

A következő olvasás majd ennél a karakternél folytatódik.

Hibás működést okoz, ha az olvasás nem tud megfelelő értéket

előállítani, például ha egy ’a’ betűt talál egy egész típusú változó

beolvasásához. Egy utasítással egyszerre, pontosabban egymás után több

változónak is lehet értéket adni: ifile>>változó1>> változó2.

C++ nyelven az olvasás nem okoz hibás működést, ha már elértük a fájl

végét. Ilyenkor egyetlen dolog történik: ettől kezdve az ifile.eof()

függvény igaz értéket ad vissza. Ügyeljünk arra, hogy mindaddig, amíg nem

hajtottunk végre olvasást, addig az ifile.eof() függvény értéke nem

megbízható. Tehát mindig előbb olvassunk, és csak utána vizsgáljuk meg az

Page 175: PROGRAMOZÁSEz 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

175

eof()-ot. Ezt biztosítja az implementációs stratégiák között említett előre

olvasási technika.

Megjegyezzük, hogy a fail() függvény általánosabb hatású, mint az

eof(), mert nemcsak fájl vége esetén, hanem egyéb olvasási hibák esetén is

igazat ad. „Bolond biztosabb” lesz az alkalmazásunk, ha a fájlvége figyelést a

fail() függvénnyel végezzük.

Ha a szöveges állomány összes karakterét egyenként kell beolvasni,

akkor vagy ki kell kapcsolni az elválasztójelek átlépését

#include <iomanip>

ifile.unsetf(ios::skipws);

char ch;

ifile >> ch;

vagy másik műveletet kell választani:

char ch;

ifile.get(ch);

Ez utóbbinak megvan a kiíró párja is: ofile.put(ch), amely

egyetlen karakter kiírásánál ugyanúgy működik, mint az ofile << ch

utasítás.

Ha a szöveges állomány összes karakterét egyenként kell beolvasni,

akkor vagy ki kell kapcsolni az elválasztójelek átlépését

#include <iomanip>

ifile.unsetf(ios::skipws);

char ch;

ifile >> ch;

vagy másik műveletet kell választani:

Page 176: PROGRAMOZÁSEz 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

176

char ch;

ifile.get(ch);

Ez utóbbinak megvan a kiíró párja is: ofile.put(ch), amely

egyetlen karakter kiírásánál ugyanúgy működik, mint az ofile << ch

utasítás.

Sokszor van szükség arra, hogy teljes sorokat olvassunk be egyben,

majd a beolvasott sorból szedjük ki a számunkra hasznos információt. A

getline() függvénnyel tudunk egy teljes sort,

string sor;

getline(ifile, sor);

vagy egy megadott jelig (karakterig) tartó karakterláncot beolvasni egy

szöveges állományból. Ez az elválasztó jel lehet a sorvége jel is, ekkor az

utasítás hatása megegyezik a fentivel.

getline(ifile, sor, '\n' );

Nem tartozik szorosan a szöveges állományok témakörébe, de mivel az

adatfolyamokkal kapcsolatos nyelvi lehetőség, ezért itt említjük meg az

úgynevezett sztring-folyamok (stringstream) használatát. Ez lehetőséget

nyújt arra, hogy egy tetszőleges sztringet, ugyanúgy, mint a konzolos input

vagy a szöveges állomány beolvasása esetén, „felszeleteljünk” és a szeleteket

megfelelő más típusú értékekké alakítsuk, vagy fordítva, ahogy a kiírásnál,

különféle értékeket egyetlen karakterláncba fűzzünk össze. A sztring-

folyamok 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

Page 177: PROGRAMOZÁSEz 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

177

ciklusmag mindig egy olvasással kezdődik, másrészt az olvasás egy logikai

értéket ad vissza, amely hamis, ha az olvasás sikertelen.

string str = "Alma a fa alatt";

istringstream is;

is.str(str);

string tmp;

while(is >> tmp) { cout << tmp << endl;}

Természetesen eltérő típusú értékek is kiolvashatóak egy sztringből,

csak tudni kell, hogy azokat milyen sorrendben fűztük korábban össze.

string str = "218 Gipsz Jakab 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();

Page 178: PROGRAMOZÁSEz 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

178

Ebben a fejezetben találkozni fogunk a struktúra nyelvi elemmel.

Ennek segítségével olyan összetett adattípust definiálhatunk, amelynek egy

értéke több komponensből áll. Egy ilyen összetett érték, más néven rekord,

komponenseire név szerint lehet hivatkozni.

Az alábbi példa egy hallgató adatait (azonosító és két osztályzat)

összefogó rekord típusát írja le:

struct Hallg {

string eha;

int jegy1, jegy2;

};

Ha h egy Hallg típusú változó, akkor a h-ban tárolt érték egy rekord,

amelynek például az azonosítójára a h.eha kifejezéssel hivatkozunk, ezt a

sztringet le lehet kérdezni, meg lehet változtatni.

Page 179: PROGRAMOZÁSEz 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

179

11. Feladat: Szöveges állomány maximális eleme

Olvassunk be egy szöveges állományból egész számokat egy tömbbe, és

válasszuk ki a tömb elemei közül a legnagyobbat, továbbá mondjuk meg,

hogy ez hányadik a számok között! Az állomány első eleme a feltöltendő

tömb hosszát mutatja, amit aztán ennek megfelelő számú egész szám követ.

Feltesszük, hogy az állományban csak egész számokat helyeztek el elválasztó

jelekkel (szóköz, tabulátor, sorvége) határolva. Az eredményt írjuk ki a

szabványos kimenetre (képernyőre)!

A feladat csak a bemenő adatok megadási módjában tér el a 6.

feladatban megoldott problémától, így a megoldás terve megegyezik az ott

látottakkal.

Specifikáció

A = ( v : ℤn, max, ind : ℤ )

Ef = ( v=v n>0 )

Uf = ( v=v’ max = v[ind] =maxn

1i

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

Page 180: PROGRAMOZÁSEz 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

180

Csak a tömb szöveges állományból történő feltöltése résszel kell

foglalkoznunk, a kód többi része megegyezik a 6. feladatnál mutatott

kódrészekkel.

Először megkérdezzük annak a szöveges állománynak a nevét,

amelyben a bemenő adatokat tároljuk. Ezt a szabványos bementről olvassuk

be egy sztringbe.

string filename;

cout << "A fájl neve:"; cin >> filename;

Ezután definiálunk és megnyitunk egy bementi adatfolyamot, amely

segítségével a szöveges állomány adatait elérhetjük. Ennek az objektumnak a

neve legyen inp, amelyet ifstream típusúnak választunk. Ennek a típusnak

a használatához hivatkoznunk kell a program elején az fstream könyvtárra

(#include <fstream>). Az inp megnyitásakor (open) kell megadnunk a

szöveges állomány már bekért nevét archaikus (C-stílusú) karakterlánc

formában: filename.c_str().

Mivel a fájl megnyitásakor különféle hibák fordulhatnak elő (nincs

állomány, más néven szerepel, nincs jogunk létrehozni, stb.), felkészülünk

azok észlelésére, és a felhasználó tájékoztatása után megkíséreljük újra a

műveletet. Az alábbi kód az input szöveges állományt nyitja meg, ennek

sikerességét az inp.fail() logikai kifejezés vizsgálatával ellenőrizzük. A

clear művelet az ismételt fájlnyitáshoz kell.

ifstream inp;

do{

string filename;

cout << "\nKérem a fájl nevét: " ;

cin >> filename;

inp.clear();

Page 181: PROGRAMOZÁSEz 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

181

inp.open(filenev.c_str());

if(inp.fail())

cout << "A megadott fájlt nem találom! \n";

}while(inp.fail());

A sikeres megnyitás után beolvashatjuk az állományban tárolt

adatokat egy tömbbe. A tömb definíciója előtt meg kell ismerni a tömb

elemeinek számát, azaz a tömb hosszát. Ebben a feladatban ezt a szöveges

állomány tartalmazza, így először onnan a feldolgozásra váró elemek számát

olvassuk be. Az olvasás módja azonos a szabványos bementről történő

olvasással, azzal a különbséggel, hogy itt nem a cin, hanem az inp bemeneti

adatfolyamot használjuk.

int n;

inp >> n;

Meg kell győződnünk arról, hogy ez az olvasás sikerült-e, azaz

inp.fail() hamis-e, valamint azt, hogy teljesül-e a darabszámra a feladat

előfeltétele: ez előírja, hogy a tömb hossza egy pozitív egész szám.

int n;

inp >> n;

if (inp.fail() || n<1){

cout << "Az állomány tartalma nem megfelelő!\n";

return 1;

}

Megjegyezzük, hogy itt szó sem lehet arról, hogy valamilyen do-while

ciklussal végezzük az adatellenőrzést. A szöveges állomány ugyanis már

Page 182: PROGRAMOZÁSEz 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

182

készen van, olvasásra megnyitottuk, ezért futás közben nincs lehetőség a

tartalmának megváltoztatására. Hibás adat esetén le kell állítani a

programot, hogy a szöveges állományt kijavíthassuk.

Megfelelő darabszám ismeretében létrehozhatjuk azt a tömböt,

amelyben a bemenő értékeket helyezzük el. Ha feltételezhetjük, hogy az

állomány elején megadott darabszám helyes, azaz a darabszámot annyi szám

követi az állományban, amennyi a darabszám értéke, akkor az alábbi kóddal

feltölthetjük a tömböt.

vector<int> v(n);

for(int i=0; i<n; ++i) inp >> v[i];

Ha a darabszám kisebb a szöveges állományban ténylegesen

elhelyezett számok számánál, akkor nem fog minden szám bekerülni a

tömbbe; ha nagyobb, akkor a tömb végén lesznek definiálatlan értékek is. Az

utóbbi esetben a maximum kiválasztás hibás eredményt is okozhat. Éppen

ezért biztonságosabb az alábbi kód, amelyik leállítja az olvasást, ha az végére

ért az állománynak, és figyeli azt is, hogy az egyes számok beolvasása

rendben történt-e.

vector<int> v(n);

for(int i=0; i<n; ++i){

inp >> v[i];

if(inp.eof()) break;

if(inp.fail()){

cout << "Az állomány tartalma nem jó!\n";

return 1;

}

}

Page 183: PROGRAMOZÁSEz 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

183

A beolvasás után a tömb elemeit kiírjuk a szabványos kimenetre.

cout << "A tömb hossza: " << n << endl;

cout << "A tömb elemei: ";

for (int i=0; i<n-1; ++i) cout << v[i] << ", ";

cout << v[n-1] << endl;

Tesztelés

A teszteléshez a 6. feladat teszteseteit használhatjuk. Ezeken kívül a

beolvasás számára kell fehér doboz teszteseteket megfogalmazni.

1. Nem létező állomány megadása illetve ismételten nem létező

állomány megadása.

2. Üres állomány.

3. Első adat nem természetes szám.

4. Első szám nem pozitív.

5. Az első szám nem az azt követő elemek darabszáma (nagyobb ill.

kisebb).

6. Az állományban számok helyett szövegek vannak.

7. Az állományban „túl nagy” szám is van.

Page 184: PROGRAMOZÁSEz 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

184

Teljes program

Tekintsük meg végül a teljes programkódot!

#include <iostream>

#include <fstream>

#include <vector>

#include <string>

using namespace std;

int main()

{

// Az állomány nevének bekérése

string filename;

ifstream inp;

do{

cout << "\nKérem a fájl nevét: " ;

cin >> filename;

inp.clear();

inp.open(filename.c_str());

if(inp.fail())

cout << "A megadott fájlt nem találom!\n";

}while(inp.fail());

// Beolvassuk és ellenőrizzük a tömb hosszát

Page 185: PROGRAMOZÁSEz 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

185

int n;

inp >> n;

if (inp.fail() || n<1){

cout << "Az állomány tartalma nem jó!\n";

return 1;

}

// Létrehozunk egy tömböt, feltöltjük, majd kiírjuk

vector<int> v(n);

for(int i=0; i<n; ++i){

inp >> v[i];

if(inp.eof()) break;

if(inp.fail()){

cout << "Az állomány tartalma nem jó!\n";

return 1;

}

}

cout << "A tömb hossza: " << n << endl;

cout << "A tömb elemei: ";

for (int i=0; i<n-1; ++i) cout << v[i] << ", ";

cout << v[n-1] << endl;

Page 186: PROGRAMOZÁSEz 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

186

// Maximum kiválasztás

int ind = 0, max = v[0];

for(int i=1;i<n;++i){

if (v[i]>max){ ind = i; max = v[i]; }

}

// Kiíratás

cout << "A tömb egyik legnagyobb eleme: "

<< max << endl;

cout << "Ez a " << (ind+1) << ". elem." << endl;

return 0;

}

Page 187: PROGRAMOZÁSEz 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

187

12. Feladat: Jó tanulók kiválogatása

Válogassuk ki egy tömbben tárolt hallgatói adatok közül a jó tanulókat! Jó

tanuló az, akinek nincs négyesnél rosszabb jegye. Ehhez ismerjük a hallgatók

kódját és két osztályzatát. Gyűjtsük ki azon hallgatók kódjait, akiknek

mindkét osztályzata legalább négyes! A tömböt egy szöveges állományból

töltsük fel. A szöveges állományban soronként helyezkednek el a hallgatók

adatai. Egy sorban az első hét karakter a kód, utána egy szóköz, azután az

első osztályzat (számjegy), majd egy szóköz, és a második osztályzat.

Specifikáció

A = ( adat : Hallgn, ki : String* )

Hallg=rec(eha:String, jegy1,jegy2:ℕ)

Ef = ( v=v’ )

Uf = ( v=v’ ki =

ehaiadat

n

jegyiadatjegyiadati

].[

].[].[

3231

1 )

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

Page 188: PROGRAMOZÁSEz 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

188

A programkód két fő részből áll: először feltöltjük az adat tömböt a szöveges

állománybeli adatokkal, utána pedig elvégezzük a kiválogatást. Külön kiírás

részre nincs szükség, ha az implementációban a ki kimeneti-változót a cout

kimeneti adatfolyammal helyettesítjük. Erre az ad lehetőséget, hogy a

kimeneti-változó egy kód sorozatot tárol és egy kód egy sztring, tehát

összességében az eredmény egy karakterfolyam, amelyet közvetlenül a cout

kimeneti adatcsatornára küldhetünk.

Először azonban definiáljuk a hallgatói adatokat tartalmazó struktúrát.

struct Hallg {

string eha;

int jegy1, jegy2;

};

A hallgató a feladat szempontjából egy összetett, több részből álló

adat: a hallgató kódja és két osztályzata. Ezt a fenti kóddal írhatjuk le. A

Hallg egy új típus, amelynek értékei a fenti három adattal rendelkező

hallgatók lehetnek. Segítségével definiálhatunk változókat

Hallg h

amelyek képesek egy kód és két osztályzat tárolására. Az egyes részekre

h.eha, h.jegy1, h.jegy2 formában lehet hivatkozni. A Hallg típust

használhatjuk egy olyan tömb deklarálására is, amelyik elemei hallgatók:

vector<Hallg> adat

Beolvasás

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

Page 189: PROGRAMOZÁSEz 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

189

tárolva a szöveges állomány elején. Szerencsére egy vector<> típusú

tömbhöz hozzá is lehet fűzni elemeket.

vector<Hallg> adat;

Hallg h;

adat.push_back(h);

Egy hallgató adatainak a szöveges állományból való beolvasására két

megoldás is kínálkozik. Az egyikben azt használhatjuk ki, hogy a szöveges

állományban a szabványos elválasztó jelekkel (szóköz, sorvége jel) vannak az

adatelemek egymástól elhatárolva.

inp >> h.eha >> h.jegy1 >> h.jegy2;

A másik módszernél azt használjuk ki, hogy az összetartozó adatok

soronként helyezkednek el, és minden soron belül rögzített egy adatelem

kezdőpozíciója és mérete. A kód a 0. pozíción kezdődik és 7 karakter hosszú,

az első jegy a 8., a második jegy a 10. pozíción található. Először beolvassuk a

teljes sort egy sztringbe,

string sor;

getline(inp, sor);

majd ebből mazsolázzuk ki az adatelemeket. A substr() egy karakterlánc

megadott pozíciójú karakterétől kezdődően megadott hosszú rész-sztringjét

adja vissza.

h.eha = sor.substr(0,7);

Ha egy adatelem nem sztring, akkor azt át is kell alakítani megfelelő típusúra

(ezt az előző módszernél a beolvasó operátor elvégezte helyettünk)

h.jegy1 = atoi(sor.substr(8,1).c_str());

h.jegy2 = atoi(sor.substr(10,1).c_str());

Page 190: PROGRAMOZÁSEz 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

190

Az atoi() helyett használható az alábbi megoldás is:

istringstream is; // #include <sstream>

is.str(sor.substr(8,1));

is >> h.jegy1;

is.clear();

is.str(sor.substr(10,1));

is >> h.jegy2;

A fenti módszerek mindegyike feltételezi, hogy a szöveges állomány

tényleg a megadott formában tárolja az adatokat.

Egyértelmű, hogy ennél a feladatnál a legelső olvasási módszer a jobb,

mert egyszerűbb. Nem működne viszont abban az esetben, ha a kódok

helyett nevek lennének a szöveges állományban, hiszen név tartalmaz

szóközt is, amely a beolvasás szempontjából elválasztó jel. (Ráadásul azt sem

tudjuk, hogy hány szóköz van egy névben, hiszen egy hallgatónak több

keresztneve is lehet.)

Az adatoknak a szöveges állományból való beolvasását végző ciklust

előre-olvasási technikával kell kódolni, azaz a ciklus előtt és a ciklus mag

végén is olvasni kell a fájlból. A ciklusfeltételben vizsgálhatjuk, hogy nem

értünk-e a fájl végére. (Erre az alábbiakban az eof() függvény helyett a

fail()-t használjuk.) Az előre-olvasást végző ciklus magjában mindig az egy

lépéssel korábban beolvasott adatokat dolgozzuk fel, azokat fűzzük hozzá a

tömbhöz.

vector<Hallg> adat;

Hallg h;

inp >> h.eha >> h.jegy1 >> h.jegy2;

while(!inp.fail()) {

Page 191: PROGRAMOZÁSEz 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

191

adat.push_back(h);

inp >> h.eha >> h.jegy1 >> h.jegy2;

}

Absztrakt program

Az absztrakt program kódolásánál figyelembe kell venni, hogy nincs szükség a

ki-t helyettesítő cout kezdeti értékadására. A hozzáfűzés műveletét a kiíró

operátor (<<) helyettesíti. Ne felejtsük el a kiírt kódokat valahogyan

elválasztani egymástól, például mindegyik kerüljön új sorba.

Ügyeljünk arra is, hogy a C++ nyelvbeli tömb 0-tól indexeli az elemeit,

ezért a tervben szereplő 1..n intervallumot a 0..n-1 intervallum váltja fel.

cout << "A jó tanulók kódjai:\n";

for(int i=0; i<(int)adat.size(); ++i){

if(adat[i].jegy1>3 && adat[i].jegy2>3){

cout << adat[i].eha << endl;

}

}

Tesztelés

A teszteléshez a 6. feladata teszteseteit használhatjuk. Ezeken kívül a

beolvasás számára kell fehér doboz teszteseteket megfogalmazni.

1. Nem létező állomány megadása illetve ismételten nem létező

állomány megadása.

2. Üres állomány.

3. Első adat nem természetes szám.

Page 192: PROGRAMOZÁSEz 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

192

4. Első szám nem pozitív.

5. Az első szám nem az azt követő elemek darabszáma (nagyobb ill.

kisebb).

6. Az elemek között van nem egész szám.

Page 193: PROGRAMOZÁSEz 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

193

Teljes program

#include <iostream>

#include <fstream>

#include <sstream>

#include <vector>

#include <string>

using namespace std;

struct Hallg{

string eha;

int jegy1, jegy2;

};

int main()

{

ifstream inp;

do{

string fajlnev;

cout << "\nKérem a fájl nevét: " ;

cin >> fajlnev;

inp.clear();

inp.open(fajlnev.c_str());

if(inp.fail()){

Page 194: PROGRAMOZÁSEz 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

194

cout << "A megadott fájlt nem találom! \n";

}

}while(inp.fail());

vector<Hallg> adat;

Hallg h;

inp >> h.eha >> h.jegy1 >> h.jegy2;

while(!inp.fail()) {

adat.push_back(h);

inp >> h.eha >> h.jegy1 >> h.jegy2;

}

cout << "A jó tanulók kódjai:\n";

for(int i=0; i<(int)adat.size(); ++i){

if(adat[i].jegy1>3 && adat[i].jegy2>3){

cout << adat[i].eha << endl;

}

}

return 0;

}

C++ kislexikon

Page 195: PROGRAMOZÁSEz 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

195

Szöveges állományok

kezelését támogató

csomag

#include <fstream>

Szöveges állományhoz

adatcsatorna definiálása

ifstream f;

ofstream f;

Szöveges állomány

megnyitása olvasásra

(output fájl ugyanígy)

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

Page 196: PROGRAMOZÁSEz 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

196

}while(!f.fail())

Olvasás szöveges

állományból

f >> valtozo;

char ch;

f.get(ch);

//#include <iomanip>

f.unsetf(ios::skipws)

char ch;

f >> ch;

string sor;

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

}

Page 197: PROGRAMOZÁSEz 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

197

Fokozatosan nyújtózkodó

tömb feltöltése szöveges

állományból előre olvasási

technikával fájl végéig

vector<Element> t;

Element item;

f >> item;

while(!f.fail()) {

t.push_back(item);

f >> item;

}

Struktúra struct Sample {

string field1;

int field2, field3;

double field4;

};

Sample v;

v.field1 = " … ";

int a = v.field3;

Konverzió sztringre #include <sstream>

ostringstream os;

os << 23.5 << "---" << 12 << ’@’

string str = os.str();

Elválasztó jelekkel tűzdelt

sztring elemeinek típusos

olvasása

string str = "Alma 2 fa -12.4";

istringstream is;

is.str(str);

Page 198: PROGRAMOZÁSEz 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

198

int n; string s; double f;

is >> word >> n >> word >> f;

Page 199: PROGRAMOZÁSEz 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

199

II. RÉSZ

PROCEDURÁLIS PROGRAMOZÁS

Egy összetett feladat megoldását jól körülhatárolható részfeladatok

megoldásainak összességeként készíthetjük el. Könyvünk első kötetében

részletesen is bemutattuk, hogyan lehet a visszavezetés programtervezési

módszerének segítségével logikailag önálló részekből felépíteni egy

programot (lásd I. kötet 6. fejezet). Ott bevezettük az alprogram fogalmát,

amelyek alkalmazásával már a tervezési szakaszban megjelent a procedurális

programozás. Az alprogramok önállóan tervezhetők és kódolhatók.

Működésük egyik jellemzője, hogy bizonyos pontokon átadják a vezérlést egy

másik programrésznek (alprogramnak), majd annak befejeződése után

folytatják a tevékenységüket, azaz a program összességében különféle

alprogramok (procedúrák) működésének láncolata.

A megvalósítás szintjén – feltéve, hogy a választott programozási nyelv

rendelkezik megfelelő nyelvi eszközökkel – az alprogramokon kívül más

jellegű programrészeket is lehet különíteni egymástól egy programban. Ilyen

lehet például egy kizárólag adatokat tároló programrész (például tömbök

gyűjteménye) vagy valamilyen szempontból egy csoportot alkotó

alprogramok listája. Ezeket a leírt kódban is jól olvashatóan elkülönülő

programrészeket moduloknak nevezzük, alkalmazásuk pedig moduláris

programozás, amely a procedurális programozás kiterjesztésének is

tekinthető. Egy moduláris program működési logikája kétszintűvé válik: külön

szintet képeznek az önmagukban vizsgálható részei, és külön vizsgálhatók

azok egymáshoz való viszonya. Ez a szemléletmód hathatósan segíti mind a

program tervezését, mind a megvalósítását, mind tesztelését.

Az programrészek kialakításánál egyszerre kell ügyelni a helyes

szétválasztásra és az elválasztott részek összekapcsolásra. Egyfelől el kell

tudni úgy különíteni ezeket egymástól, hogy egy-egy résznek bármilyen

változtatása csak lokális következményekkel járjon a program egészére nézve

és ennek az elvnek következményeként minél közelebb kerüljön egymáshoz

a programrész működési logikája és az általa használt adatok. Másfelől meg

kell oldani az elkülönített programrészek közötti adat- és

Page 200: PROGRAMOZÁSEz 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

200

üzenetkommunikációt. Fontos, hogy az egyes részek közötti kapcsolat

egyszerű, jól átlátható legyen, minél inkább szolgálja az egyes részek

önállóságát, és szoros legyen az elkülönített részeken belüli összetartozás.

Már a korai programozási nyelvek is rendelkeztek olyan nyelvi

elemekkel, amelyekkel egy-egy részprogramot ki lehetett jelölni egy

programon belül. A mai nyelvekben már számos eszköz található a program

egyes részeinek leírására. Végső soron már ilyen az is, ha kommentekkel

elhatárolva különítünk el egy-egy kódrészletet vagy úgynevezett utasítás

blokkba zárjuk azt, de a procedurális programozás elsőszámú nyelvi elemei

kétségkívül az alprogramokat kódszinten kifejezni képes programfüggvények

és eljárások.

Az által, hogy a programunk hierarchiája a részekre bontás során

bonyolultabbá válik, még inkább előtérbe kerül annak az igénye, hogy egy-

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

Page 201: PROGRAMOZÁSEz 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

201

A programjaink modulokra bontása az alkalmazásaink tesztelésére is

kihat. A tesztelést is két szinten végezhetjük: külön-külön tesztelhetőek az

egyes programrészek (ezt általában modultesztnek hívjuk, ami ebben a

részben az egyes alprogramok tesztelését jelenti), majd külön azok

kapcsolatainak tesztelése. Fontos, hogy egy alprogram tesztelésénél olyan

esetekre is gondoljunk, amely az alkalmazás futtatása során ugyan soha nem

állhatna elő, mert az alprogram hívására olyan környezetben kerül sor, amely

eleve kizárja bizonyos paraméterek kipróbálását. Ha azonban az alprogramot

a környezetéből kiragadva egy másik alkalmazásban is fel akarjuk használni,

nem lenne jó, ha a futása produkálhat teszteletlen eseteket is. Ezért sok

esetben egy alprogram teszteléséhez külön tesztkörnyezetet, az alprogramot

meghívó speciális főprogramot kell készíteni.

Page 202: PROGRAMOZÁSEz 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

202

6. Alprogramok a kódban

Programjainkban a funkcionálisan összetartozó utasításokat, azaz egy-egy

részfeladat megoldásáért felelős programrészeket külön egységekbe,

úgynevezett alprogramokba (procedúrákba) szervezhetjük. Egy alprogram

olyan önálló kódrész, amelyiknek a végrehajtását egy speciális utasítás, az

alprogram hívása kezdeményez. Ekkor átkerül a vezérlés az alprogramhoz,

majd annak befejeződésekor a hívó utasítás utáni pontra tér vissza.

Egy alprogram nem független az őt tartalmazó programtól, adatokat

kap tőle és adatokat ad neki vissza. Az adatáramlás többféle módon is

megvalósulhat az alprogram és környezete között. Ha az alprogramba

bekerülő és belőle kikerülő adatokat egyértelműen jellemezzük, akkor

paradox módon egy alprogramokra tagolt program sokkal erősebb kohéziót

valósít meg a program egyes részei között, mint egy monolit, alprogramokra

nem tördelt program, mert felügyeltté válik az egyes részek közötti

adatcsere.

Implementációs stratégia

Egy programkódban többféle szempont alapján lehet alprogramokat

kialakítani.

6-1. ábra. Alprogram kialakításának okai

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.

3. Önmagában átlátható, áttekinthető kódrész összefogása.

Page 203: PROGRAMOZÁSEz 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

203

Sok alprogram már a tervezés során körvonalazódik, hiszen ekkor derül

ki, hogy a programnak milyen funkciókat ellátó részei vannak. Nem

törvényszerű, de ajánlott, hogy ezek a funkcionálisan elkülöníthető

programrészek az implementáció során is külön alprogramokat alkossanak.

Az implementáció során olyan további funkciók megvalósítására is sor

kerülhet (például adatbeolvasás, adatkiírás), amelyről a tervezés során még

nem beszélünk. Ezeket is célszerű külön alprogramokba szervezni.

Amikor a kódban több helyen is ismétlődő kódrészletet találunk, akkor

érdemes azokat egy alprogramba összevonni, és ahol szükség van rá, onnan

meghívni. Így rövidebb lesz a kód, de a sokkal fontosabb szempont az, hogy

ha javítani kell egy ilyen ismétlődő kódrészben, akkor azt, annak alprogramba

szervezése után, csak egy helyen kell megtenni. Az ismétlődő kódrészek

alprogramba történő kiemelése akkor is követendő út, ha az így kiváltott

kódrészek kismértékben eltérnek egymástól (például ugyanazt a

tevékenységet más változókon végzik vagy tevékenységük kimenetele egy

adat értékétől függ, stb.). Megfelelő általánosítással ugyanis akár egészen

különböző kódrészeket is ki lehet váltani egyetlen, jól paraméterezhető

alprogrammal. Természetesen ilyenkor a hívásnál a működést befolyásoló

információkat át kell adunk az alprogramnak. Az már nehezen dönthető el,

nem is lehet rá receptkönyvszerű választ adni, hogy meddig érdemes

elmenni a kódrészek ilyen általánosításának irányába.

Sokszor alkalmazott implementációs elv az, hogy egy alprogram kódját

egyszerre lássuk fejlesztés közben a képernyőn. Ha ez nem állna fenn, akkor

tagoljuk a kódot részekre, a részeket csomagoljuk külön alprogramokba.

Ennek az elvnek az alkalmazása azt eredményezi, hogy egy alprogramban

nem fog egy-két ciklusnál több szerepelni, és ha a ciklus magja túl nagy

lenne, akkor azt is külön alprogramba, alprogramokba tagoljuk.

Fontos kérdés, hogy egy alprogram hogyan tart kapcsolatot a program

többi részével, hogyan valósul meg az adatáramlás a hívó program és a hívott

alprogram között.

Az egyik lehetőség erre a globális változók használata. Globális változó

az, amelyet az alprogramon kívül definiálunk, de az alprogramban is látható.

(Egy változó globális jelzője relatív fogalom, mindig egy alprogram vagy

egyéb programblokk szempontjából értelmezhetjük.) Ha egy változó két

Page 204: PROGRAMOZÁSEz 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

204

alprogramra nézve is globális, akkor azt mindkettő használhatja: olvashatja

és felülírhatja. A globális változók használata első látásra egy igen egyszerű

formája az adatcserének, de nagymértékben rontja a program

áttekinthetőségét. Egy alprogram működésének megértését ugyanis

akadályozza, ha minduntalan ki kell tekinteni az alprogramból, és megnézni,

hogy egy alprogramban használt globális változó a program melyik részén

keletkezett, mikor, milyen értéket kapott, hol lesz még felhasználva, stb.

Ezért csak nagyon indokolt esetben engedélyezzük a globális változók

használatát. Ilyen lehet például az, ha egy adatot az összes alprogram

használja, és megjegyzésként pontosan megjelöljük, hogy melyik alprogram

olvassa ezt az adatot, melyik az, amelyik meg is változtatja, továbbá erősen

korlátozzuk az adat értékét megváltoztató alprogramok számát.

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

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,

amelynek az alprogram befejeződésekor az értéke visszakerül a környezet

egy változójába.

Page 205: PROGRAMOZÁSEz 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

205

egy kifejezésének (akár egyetlen változójának) értékét kapják kezdőértékül.

Eredmény paraméterváltozó az, amelyik az alprogram befejeződésekor

visszaadja az értékét a hívás helyére, a hívó program egy arra kijelölt

változójának. Egy paraméterváltozó lehet egyszerre bemenő- és eredmény

paraméterváltozó is. Ebben az esetben a hívó program ugyanazon

változójának adja vissza az értékét, amelytől a kezdőértéket kapta.

Az alprogram hívásánál az úgynevezett aktuális paraméterlistát kell

megadni, amely tartalmazza azokat a kifejezéseket, amelyek értékét a

bemenő paraméterváltozóknak szánjuk, illetve azokat a változókat, amelyek

az eredmény paraméterváltozóktól kapják majd az értéküket. A hívásnak

egyértelműen ki kell jelölnie, hogy melyik paraméter melyik

paraméterváltozóhoz tartozik. Ezt általában a paraméterek sorrendje

határozza meg. Ügyelni kell arra, hogy egy paraméter típusa megegyezzen

(legalább kompatibilis legyen) a neki megfeleltetett paraméterváltozó

típusával.

Egy alprogramot kétféleképpen hívhatunk meg. Függvényszerű hívása

esetén az alprogramot függvénynek szokták nevezni, egyébként pedig

eljárásnak. Eljárásként hívott alprogram esetén a hívás egy önálló utasítás,

míg a függvény hívása egy utasításba ágyazott kifejezés. Mindkettő az

alprogram nevéből, és az aktuális paraméterlistából áll. A függvényt hívó

kifejezésnek maga a függvény ad értéket. Ehhez a függvény definiálásakor fel

kell tüntetni a visszatérési érték típusát (esetleg típusait), és a kódjában

egyértelműen jelölni kell azt, hogy leállásakor milyen érték adódjon vissza a

hívás helyére.

Elsősorban implementációs döntés (bár a tervezés is utalhat rá) az,

hogy egy alprogramot függvényként vagy eljárásként kódoljunk-e.

Szerencsére viszonylag könnyen lehet egy alprogramot átalakítani

függvényből eljárásba és viszont. A döntésre hatással van az, hogy a

választott programozási nyelv mit enged meg. Van ugyanis olyan nyelv,

amely függvényei egyetlen „egyszerű” típusú visszatérési értékkel

rendelkezhetnek csupán, van olyan, amelyikben a visszatérési érték mellett

eredmény paraméterváltozó is használható, akad olyan is, amelyikben csak

függvényeket használhatunk.

Nyelvi elemek

Page 206: PROGRAMOZÁSEz 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

206

A program alprogramokra tördelését a magas szintű programozási nyelvek

különleges nyelvi elemekkel támogatják (szubrutin, eljárás, függvény). Egy

alprogram egy programozási nyelvben egy deklarációból (fejből) és egy

törzsből áll. A deklarációt és a törzset együtt az alprogram definíciójának

hívjuk. A deklaráció tartalmazza az alprogram nevét, a formális

paraméterlistát és az esetleges visszatérési érték típusát. Ha a deklarációból

elvesszük a függvény nevét, akkor az így kapott maradékot a függvény

típusának hívjuk. A törzs egy olyan programblokk, melynek végrehajtását a

program bármelyik olyan helyéről lehet kezdeményezni, ahol az alprogram

neve érvényes (ahová az alprogram hatásköre kiterjed).

Az alprogram hívása az alprogram nevével történik. Ilyenkor a

vezérlés (az utasítások végrehajtásának menete) átadódik a hívás helyéről az

alprogram első utasítására. A hívó program további végrehajtása mindaddig

szünetel, amíg az alprogramhoz tartozó kód le nem fut. Az alprogram akkor

fejeződik be, ha az összes utasítása végrehajtódott vagy egy befejezését

előíró speciális return utasításhoz nem ér. Ekkor a program végrehajtása

visszakerül a hívás helyére. A hívó utasításban az alprogram neve mellett kell

felsorolni az aktuális paramétereket, amelyek számra, sorrendre (bizonyos

programozási nyelveknél ez nem kötelező) és típusra meg kell, hogy

egyezzenek a formális paraméterlista változóinak típusával. (Vannak olyan

programozási nyelvek, ahol a formális paramétereknek lehetnek

alapértelmezett értékeik. Ilyenkor az aktuális paraméterlista rövidebb lehet a

formális paraméterlistánál.) Egy bemenő adatként szereplő aktuális

paraméter lehet a hívó programrész egy változója vagy egy kifejezés,

ellenben a visszakapott adatként szereplő paraméter mindig a hívó

programrész egy változója kell legyen.

A paraméterváltozók az alprogram lokális változói, csak az

alprogramon belül érvényesek (csak az alprogram törzsében lehet rájuk

hivatkozni, az alprogram futási ideje alatt foglalnak helyet a verem

memóriában). Ezek mellett az alprogramnak lehetnek egyéb lokális változói

is. A paraméterváltozókat csak az különbözteti meg a többi lokális változótól,

hogy az alprogram hívásakor illetve befejeződésekor adatkapcsolatot

létesítenek a hívó környezettel.

Page 207: PROGRAMOZÁSEz 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

207

A C++ nyelv kétféle paraméter átadási módot ismer. Érték szerinti

paraméterátadással a bemenő adatot tudjuk a bemenő

paraméterváltozóhoz eljuttatni, a hivatkozás (referencia) szerinti

paraméterátadás mindkét irányú adatáramlást támogatja.

Érték szerinti paraméterátadás valósul meg az alábbi kódban. Itt az x

változó egy bemenő paraméterváltozó. Híváskor az v változó értéke

átmásolódik az x változóba, amely egy önálló memória területtel rendelkező

lokális változója a függvénynek. A hívás után már semmi kapcsolat nincs a v

és az x változók között.

hívás: int v = 23; fv(v);

hívott: void fv(int x) { }

Paraméterátadási módok

Érték szerinti paraméterátadás

Az aktuális paraméter értéke az alprogram hívásakor átmásolódik a neki

megfeleltetett paraméterváltozóba, és annak kezdőértékévé válik. A

paraméterváltozó ettől eltekintve a hívó programrésztől teljesen függetlenül,

az alprogram lokális változójaként használható.

Cím vagy hivatkozás (referencia) szerinti paraméterátadás

Az alprogram paraméterváltozója nem rendelkezik olyan önálló

memóriafoglalással, ahová az aktuális paraméter értékét át lehetne másolni.

A paraméterváltozó vagy egy pointer, amelyik a memóriafoglalás címét kapja

értékül (cím szerinti paraméterátadás), vagy egy úgynevezett

referenciaváltozó, amelyik az aktuális paraméterként megadott változó

memóriafoglalását használja, mintha annak a változónak egy másodlagos

(alias) neve lenne (hivatkozás szerinti paraméterátadás).

Érték-eredmény szerinti paraméterátadás

Az érték szerinti paraméterátadást kombináljuk azzal, hogy amikor az

alprogram befejeződik, akkor az aktuális paraméterként megadott változóba

átmásolódik a megfeleltetett paraméterváltozó értéke. Ettől eltekintve a

paraméterváltozó teljesen különálló változóként, az alprogram lokális

változójaként használható.

Page 208: PROGRAMOZÁSEz 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

208

Név szerinti paraméterátadás

A paraméterváltozó egy sablon, amely helyébe egy-egy konkrét hívás

ismeretében szövegszerűen másolódik be a megfeleltetett aktuális

paraméter.

Hivatkozás szerinti paraméterátadás történik az alábbi példában. Az x

paraméterváltozó nem egy önálló új változó, hanem egy úgynevezett

referenciaváltozó, amelyik azonos memória területet használ a hívás helyén

feltüntetett a v változóval. Bármit változtat az alprogram az x változóban az

a változás v változóban is megjelenik. Az x változó valójában egy bemenő- és

eredmény paraméterváltozó egyben, hiszen az előbb leírtak szerint már a

létrejöttekor (híváskor) látható x változóban a v értéke, és a hívó programba

történő visszatérés után a v változóban a x értéke.

hívás: int v = 23; fv(v);

hívott: void fv(int &x) { }

Általános szabályként alkalmazzuk a C++ nyelvben azt, hogy a kizárólag

bemenő paraméterváltozókhoz az érték szerinti paraméterátadást, az

eredmény paraméterváltozókhoz pedig a hivatkozás szerinti

paraméterátadást használjuk. Hatékonysági okból azonban célszerű az

összetett típusú bemenő paraméterváltozóknál is a hivatkozás szerinti

paraméterátadást alkalmazni. Az érték szerinti paraméterátadás esetén

ugyanis mindig lemásolódik az átadott paraméter értéke, hiszen ilyenkor a

paraméterváltozó önálló memóriafoglalással rendelkezik. A memóriafoglalás

ilyen duplázása nem okoz gondot egy egyszerű (tehát néhány bájtot

elfoglaló) adat esetén, de összetett típusú adatoknál ez már nagy veszteséget

jelenthet. Ezt elkerülendő, ilyenkor is hivatkozás szerinti paraméterátadást

alkalmazunk. Ennek a megoldásnak a szépséghibája az, hogy egy ilyen

paraméterváltozó egyben eredmény paraméterváltozó is lesz, tehát képes a

neki átadott paramétert megváltoztatni. Hogy ez mégse történjen meg (nem

elégszünk meg a programozó „isten bizony nem változtatom meg”

ígéretével) a const kulcsszót írjuk a paraméterváltozó típusa elé. Ennek

eredményeként fordítási hibát fogunk kapni, ha az alprogramban meg

akarnánk változtatni az ilyen paraméterváltozó értékét. Összefoglalva tehát,

Page 209: PROGRAMOZÁSEz 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

209

az összetett típusú kizárólag bemenő paraméterváltozókat konstans

referenciaváltozóként deklaráljuk. (Külön szabály vonatkozik az eredeti C++-

os tömb típusú paraméterekre, de mivel helyettük a vector típust

használjuk, ezért erre itt nem térünk ki.)

A C++ nyelvben minden alprogramot függvénynek nevezünk. A

függvény fejében legelőször a visszatérési érték típusát kell megadni, majd

azt követően a függvény nevét, azután a paraméterlistáját. A függvénynek

egyetlen, de tetszőleges típusú visszatérési értéke lehet. Ha több értéket kell

visszaadni, akkor azokat összefoghatjuk egy tömbbe vagy egy rekordba (egy

struktúrába), és így formailag egyetlen értéket adunk vissza. A függvény

törzse kapcsos zárójelek közé kerül. A visszatérési értéket a függvény

kódjában egy kitüntetett utasításban (úgynevezett return utasításban)

elhelyezett kifejezés formájában kell leírni. Ennek az utasításnak a

végrehajtása a függvény befejeződését vonja maga után. Az eljárásokat úgy

jelöljük, hogy a visszatérési típus helyére a void kulcsszót írjuk. Ilyenkor nem

kell return utasítást használni, de üres return utasítással kikényszeríthetjük az

eljárás befejeződését. Tehát C++ nyelven az eljárás egy visszatérési érték

nélküli függvény.

Mi történik egy alprogram hívásakor?

Amikor egy alprogram hívására sor kerül, annak érdekében, hogy majd a

meghívott alprogram befejeződésekor vissza lehessen térni a hívó

programrészhez, számos, a hívó programrész működésével kapcsolatos

információ elmentésre kerül. A hívó programrész lokális változói már a hívás

előtt a verem memóriában (STACK) vannak. A híváskor ide másolódnak a

központi egység regisztereinek értékei is, mint például az utasítás számláló

(PC), utasítás regiszter (IR), akkumulátor (AC) stb. Csak ezeket követően

kerülnek a verem memóriába a hívott alprogrammal kapcsolatos adatok,

például ekkor foglalnak itt automatikusan helyet a meghívott alprogram

lokális változói, köztük a paraméterváltozói is, és a bemenő

paraméterváltozók megkapják kezdőértéküket

A return utasítás hatására a verem memóriából törlődnek a hívott

alprogrammal kapcsolatos adatok miután a kimenő paraméterváltozók

Page 210: PROGRAMOZÁSEz 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

210

érékei és az esetleges visszatérési érték a hívó program megfelelő helyére

került, a verem memóriából visszatöltődnek a korábban elmentett értékek a

regiszterekbe, hogy a hívó programrész folytatódhasson.

A bemutatott programjainkban már eddig is használtunk függvényt,

hiszen a main() egy olyan „alprogram”, amelyet az operációs rendszer hív

meg, befejeződésekor oda tér vissza a vezérlés. A main függvénynek lehet

bemenő adatokat is adni, amelyeket például egy parancssorból történő

futtatás kezdeményezésekor adhatunk meg, és a visszatérési értékét is le

lehet kezelni.

Page 211: PROGRAMOZÁSEz 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

211

13. Feladat: Faktoriális

Számoljuk ki egy adott természetes szám faktoriálisát!

Specifikáció

A feladat specifikációjában a faktoriális számításnak az eredeti definícióját

adjuk meg, nevezetesen, hogy n! = 2 * 3 * … * n. A 0 és az 1 faktoriálisa

definíció szerint 1.

A = ( n : ℕ, f : ℕ )

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

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

Page 212: PROGRAMOZÁSEz 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

212

A program kerete

A main függvény beolvas a billentyűzetről egy természetes számot,

kiszámolja a faktoriálisát, majd kiírja az eredményt a konzol ablakba.

int n = ReadNat("Kérem a számot: ",

"Természetes számot kérek!");

int f = Factorial(n);

cout << "Faktorialis = " << f;

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,

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{

Page 213: PROGRAMOZÁSEz 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

213

cout << "Faktoriális = " << Factorial(

ReadNat("Kérem a számot: ",

"Természetes számot kérek!" ));

cout << "\n\nFolytatja? (I/N)"; cin >> ch;

getline(cin,tmp);

}while(ch != 'n' && ch != 'N');

return 0;

}

Az alprogramok közötti hívási kapcsolatot teszi szemléletessé az alábbi

ábra. A nyilak jelzik, hogy melyik alprogram hívja a másikat. A nyilak mellé

oda lehetne még írni az adatforgalmat is: milyen bemenő adat adódik át

híváskor, és milyen eredményt szolgáltat a meghívott alprogram.

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.

main()

ReadNat()

Factorial()

Page 214: PROGRAMOZÁSEz 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

214

Absztrakt program kódolása

Az absztrakt program alapján a Factorial() függvény törzsét készíthetjük

el, annak kódja a struktogramm szó szerinti lefordításának eredménye.

int Factorial(int n)

{

int f = 1;

for(int i = 2; i<=n; ++i){

f = f * i;

}

return f;

}

Az absztrakt program három változót használ. Az n változó a

Factorial függvény bemenő paraméterváltozója lesz. Ez lokális a

függvényre nézve, de kezdőértékét a hívás helyéről kapja. Ehhez érték

szerinti paraméterátadást alkalmazunk. A Factorial függvény az absztrakt

program eredményét visszatérési értékként juttatja el a hívás helyére, ezért

az eredmény változót a függvényben lokális változóként definiáljuk és a

program végén a return utasítással adjuk vissza az értékét. Az absztrakt

program lokális i változója a C++ függvénynek is lokális változója lesz,

ráadásul a láthatóságát a ciklusra korlátozzuk.

Elvileg elkészíthetnénk a Factorial függvényt úgy is, hogy az

eredményt paraméterváltozón keresztül adja vissza. Ehhez az f-et hivatkozás

szerinti paraméterváltozóként kellene definiálni az n bemenő

paraméterváltozó mellett. Ilyenkor nincs szükség a return utasításra hiszen a

függvény visszatérési típusa void. Ennek a megoldásnak a hátránya az, hogy

ezt eljárásszerűen kellene a main függvényből meghívni:

Factorial(n, f);

Page 215: PROGRAMOZÁSEz 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

215

Beolvasás

A beolvasáshoz használt függvény két paraméterváltozóval

rendelkezik. Mindkettőt kizárólag bemenő paraméterváltozóként fogjuk

használni, de mivel összetett (sztring) típusúak, ezért nem érték szerinti,

hanem konstans hivatkozás szerinti paraméterátadással oldjuk meg a kezdeti

értékadásukat.

int ReadNat(const string &msg, const string &errmsg)

{

int n;

bool error = true;

do{

cout << msg; cin >> n;

if(error = cin.fail() || n<0){

cout << errmsg << endl;

cin.clear();

}

string tmp; getline(cin,tmp);

}while(error);

return n;

}

A függvény törzsében a korábban már ismertetett technikával

próbálunk egy billentyűzetről egy nem negatív számot beolvasni. Ha a

megadott adat nem szám vagy negatív, hibajelzést adunk, és újra

megkíséreljük a beolvasást. Ennél fogva ennek a függvénynek csak akkor

fejeződik be a végrehajtása, ha sikerül egy természetes számot beolvasni.

Page 216: PROGRAMOZÁSEz 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

216

Tesztelés

A teszteseteket alprogramonként, azaz modulonként adjuk meg.

A Factorial()alprogram az absztrakt program kódja, ezért ennek

fekete doboz tesztesetei a feladat specifikációjából nyerhető érvényes

tesztesetek. A kód egyszerűsége miatt ezek egyben a fehér doboz tesztelés

szempontjainak is megfelelnek:

1. Szélsőséges adatok ( 0! = 1, 1! = 1, 2! = 2)

2. Általános adat (4! = 24)

3. Skálázás: legkisebb olyan szám keresése, amelyik faktoriálisa már

nem ábrázolható az int típussal. (Próbáljuk meg a long int vagy a

double típust használni az eredmény változóra. Látni fogjuk, hogy

ezek sem kielégítőek, valójában egy igazán nagy számokat ábrázolni

képes egész szám típusra lenne itt szükség, de ilyen a C++ nyelvben

nincs. Mi készíthetnénk ilyet, de ennek technikájáról a könyv

harmadik részében lesz majd csak szó.)

A ReadNat() feladata egy természetes szám beolvasása, de különösen

az ebből a szempontból érvénytelen adatok lekezelése. A fehér doboz

teszteléséhez többször kell egymás után érvénytelen adatot beírni.

1. Természetes számok (0, 1 és ennél nagyobb) beolvasása

2. Negatív egész számok esete.

3. Nem egész szám esete.

A main() fekete doboz tesztelésénél a feladat érvénytelen tesztadatait

kell kipróbálni, de ez itt már nem vezet újabb tesztesetekhez. Fehér doboz

tesztesetekkel a do-while ciklust kell lefedni (a ciklusmag egyszer illetve

többször fusson le).

Page 217: PROGRAMOZÁSEz 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

217

Teljes program

#include <iostream>

#include <string>

using namespace std;

int ReadNat(const string &msg,

const string &errmsg);

int Factorial(int n);

int main()

{

cout << "Egy szám faktoriálisát számolom?\n";

string tmp;

char ch = 'i';

do{

cout << "Faktoriális = " << Factorial(

ReadNat("Kérem a számot: ",

"Természetes számot kérek!" ));

cout << "\n\nFolytatja? (I/N)"; cin >> ch;

getline(cin,tmp);

}while(ch != 'n' && ch != 'N');

return 0;

}

int Factorial(int n)

Page 218: PROGRAMOZÁSEz 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

218

{

int f = 1;

for(int i = 2; i<=n; ++i){

f = f * i;

}

return f;

}

int ReadNat(const string &msg, const string &errmsg)

{

int n;

bool error = true;

string tmp;

do{

cout << msg; cin >> n;

if(error = cin.fail() || n<0){

cout << errmsg << endl;

cin.clear();

}

string tmp; getline(cin,tmp);

}while(error);

return n;

}

14. Feladat: Adott számmal osztható számok

Page 219: PROGRAMOZÁSEz 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

219

Válogassuk ki egy egész számokat tartalmazó tömb adott k számmal

osztható elemeit egy sorozatba!

Specifikáció

A feladat egy egész számokból álló sorozatot kíván összefűzni a tömb k-val

osztható elemeiből.

A = ( t : ℤn, k : ℤ, s : ℤ

* )

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

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

Page 220: PROGRAMOZÁSEz 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

220

A tömböt vector<int> típussal fogjuk ábrázolni, amely 0-tól

kezdődően indexelődik, ezért az absztrakt program ciklusváltozója a 0..n-1

intervallumot fogja befutni.

Az eredmény sorozatot a cout adatfolyammal valósítjuk meg, így a <<

operátor helyettesíti a specifikációban használt operátort. Ennek

következményeként az eredmény sorozathoz hozzáfűzött elemek közvetlenül

a konzolablakban jelennek majd meg.

Bevezetünk három függvényt. Az egyikben az állomány megnyitására

és a tömb feltöltésére (FillVector) kerül sor, a másikban egy nem-nulla

egész szám beolvasására (ReadNotNullInt), a harmadikban (Selecting)

az absztrakt programot kódoljuk.

A program kerete

Egy forrásállományban (legyen ennek a neve main.cpp) helyezzük el a main

függvényt, amelyet megelőz az iostream, az fstream és a vector csomag

bemásolása, a using namespace std utasítás, majd a függvények

deklarációi, amelyek definíciója a main függvény után helyezkedik el.

#include <iostream>

#include <fstream>

#include <vector>

#include <string>

using namespace std;

vector<int> FillVector();

int ReadNotNullInt(const string &msg,

const string &errmsg);

void Selecting(const vector<int> &t, int k);

Page 221: PROGRAMOZÁSEz 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

221

int main()

{

cout << "Adott számmal osztható számok\n";

vector<int> t = FillVector();

int k = ReadNotNullInt("\nKérem az osztót: ",

"Egész számot kérek!\n");

cout << "Eredmény: ";

Selecting(t,k);

return 0;

}

A main függvény kódjából jól látható, hogy a program mely

függvényeket hívja meg.

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

main()

FillVector()

ReadNotNullInt()

Selecting()

Page 222: PROGRAMOZÁSEz 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

222

A FillVector() függvény egy vector<int> típusú objektumot ad vissza,

amely előállítását egy szöveges állomány adatai alapján végzi. Először

megnyitja a megfelelő szöveges állományt olvasásra, ehhez beolvassa az

állomány nevét, és ellenőrizzük, hogy azt jól adták-e meg.

vector<int> FillVector()

{

ifstream f;

bool error = true;

string str;

do{

cout << "Fájl neve: "; cin >> str;

f.open( str.c_str() );

if ( error = f.fail() ){

cout << "Nincs ilyen fájl!" << endl;

f.clear();

}

}while(error);

Az állományban elhelyezett első számadat a létrehozandó tömb

hosszát tartalmazza, ezért ezt beolvasva definiálhatjuk a tömböt. Ezt

követően töltjük fel a tömböt az állományban található egész számokkal,

majd lezárjuk az állományt.

int n;

f >> n;

vector<int> t(n);

Page 223: PROGRAMOZÁSEz 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

223

for(int i=0; i<n; ++i){

f >> t[i];

}

f.close();

return t;

}

Nullától különböző egész szám beolvasása

Ez tulajdonképpen az előző feladatban bemutatott ReadNat() módosítása,

ahol a beolvasott egész szám vizsgálatánál hibás adatnak nem az n<0

feltételt kielégítő egész számot, hanem a 0==n feltételnek megfelelő számot

tekintjük.

int ReadNotNullInt(const string &msg,

const string &errmsg)

{

int n;

bool error = true;

string tmp;

do{

cout << msg;

cin >> n;

if(error = cin.fail() || 0 == n){

cout << errmsg << endl;

Page 224: PROGRAMOZÁSEz 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

224

cin.clear();

}

getline(cin,tmp);

}while(error);

return n;

}

Kiválogatás

A megoldás harmadik része az absztrakt program kódját tartalmazó

alprogram, amelyet eljárásszerűen hívunk meg. Két, kizárólag bemenő

paraméterváltozója lesz: az egyik a bemenő adatokat tartalmazó tömb, a

másik az oszthatóság vizsgálatához megadott nem nulla egész szám. Az

elsőnek összetett típusa van, ezért annak az értékét konstans referenciaként,

a másodiknak a típusa egyszerű, azt érték szerint fogjuk átvenni.

void Selecting(const vector<int> &t, int k)

{

for(int i = 0; i<(int)t.size(); ++i){

if(t[i]%k == 0) cout << t[i] << " ";

}

}

Tesztelés

A Selecting() teszteléséhez a feladat érvényes fekete doboz

teszteseteit használjuk, külön fehér doboz tesztesetekre nincs szükség:

Page 225: PROGRAMOZÁSEz 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

225

1. Üres tömb esete. (válasz: üres sorozat)

2. Tetszőleges tömb, az osztó 1. (válasz: tömb minden eleme)

3. Egy elemű tömb, az osztó ez az elem. (válasz: ez az elem)

4. Egy elemű tömb, az osztó ez az elem+1. (válasz: üres sorozat)

5. Tetszőleges tömb, az osztó a legnagyobb elem+1. (válasz: üres

sorozat)

6. Több elemű tömb, csak az első eleme osztható az adott számmal.

7. Több elemű tömb, csak az utolsó eleme osztható az adott számmal.

8. Negatív osztó, negatív elemek a tömbben.

A ReadNotNull() az érvényes osztó előállítását végzi. Fekete doboz

tesztelése az érvénytelen adatokkal történik:

1. Nulla osztó esete.

2. Nem számként megadott osztó esete.

A ReadNotNull() fehérdoboz teszteléséhez többször kell egymás után

rossz adatot megadni:

1. Egymás után több rossz próbálkozás esete.

A FillVector() garantáltan érvényes adatokat feltételez, ezért csak

fehér doboz tesztesetek tartoznak hozzá:

1. Nem létező állománynév esete.

2. Egymás után több rossz állománynév beírása.

3. Különféle elválasztó jelek alkalmazása a szöveges állományban.

4. Nulla elemű, egy elemű, és egy sok elemű tömb előállítása.

A main() fekete doboz tesztelésénél a feladat érvénytelen tesztadatait

kell csak kipróbálni, de ez itt már nem vezet újabb tesztesetekhez.

Page 226: PROGRAMOZÁSEz 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

226

Teljes program

#include <iostream>

#include <fstream>

#include <vector>

#include <string>

using namespace std;

vector<int> FillVector();

int ReadNotNullInt(const string &msg,

const string &errmsg);

void Selecting(const vector<int> &t, int k);

int main()

{

cout << "Adott számmal osztható számok\n";

vector<int> t = FillVector();

int k = ReadNotNullInt("\nKérem az osztót: ",

"Egész számot kérek!\n");

cout << "Eredmény: ";

Selecting(t,k);

return 0;

}

void Selecting(const vector<int> &t, int k)

Page 227: PROGRAMOZÁSEz 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

227

{

for(int i = 0; i<(int)t.size(); ++i){

if(t[i]%k == 0) cout << t[i] << " ";

}

}

int ReadNotNullInt(const string &msg,

const string &errmsg)

{

int n;

bool error = true;

string tmp;

do{

cout << msg;

cin >> n;

if(error = cin.fail() || 0 == n){

cout << errmsg << endl;

cin.clear();

}

getline(cin,tmp);

}while(error);

return n;

}

vector<int> FillVector()

Page 228: PROGRAMOZÁSEz 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

228

{

ifstream f;

bool error = true;

string str;

do{

cout << "Fájl neve: ";

cin >> str;

f.open( str.c_str() );

if ( error = f.fail() ){

cout << "Nincs ilyen fájl!" << endl;

f.clear();

}

}while (error);

int n;

f >> n;

vector<int> t(n);

for(int i=0; i<n; ++i){

f >> t[i];

}

f.close();

return t;

}

Page 229: PROGRAMOZÁSEz 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

229

Page 230: PROGRAMOZÁSEz 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

230

15. Feladat: Páros számok darabszáma

Számoljuk meg egy egész számokat tartalmazó tömb páros elemeit!

Specifikáció

A feladat egy számlálás.

A = ( t : ℤn, s : ℕ )

Ef = ( t=t’ )

Uf = ( t=t’ s =

n

iti][21

1 )

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.

Page 231: PROGRAMOZÁSEz 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

231

A tömböt vector<int> típussal fogjuk ábrázolni, amely 0-tól

kezdődően indexelődik, ezért az absztrakt program ciklusváltozója a 0..n-1

intervallumot fogja befutni.

Bevezetünk két függvényt is. Az egyiket az állomány megnyitására és a

tömb feltöltésére (FillVector), a másikat az absztrakt programot

kódolására használjuk.

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

main()

FillVector()

Count()

Page 232: PROGRAMOZÁSEz 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

232

int main()

{

cout << "Megszámoljuk a páros számokat.\n";

vector<int> t;

FillVector(t);

cout << "A páros elemek száma: " << Count(t);

return 0;

}

Absztrakt program kódolása

Az absztrakt program kódja alkotja a Count() törzsét.

int Count(const vector<int> &t)

{

int s = 0;

for(int i = 0; i<(int)t.size(); ++i){

if(t[i]%2 == 0) ++s;

}

return s;

}

Page 233: PROGRAMOZÁSEz 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

233

Tömb beolvasása

Ez a kódrész hasonlít az előző feladatban is használt tömb feltöltéshez. Egy

ponton tér el csak attól: most ugyanis azt tételezzük fel a bemenő adatokat

tartalmazó szöveges állományról, hogy nem tartalmazza az adatok számát,

így a tömböt egy fájlvégéig tartó olvasással fokozatosan nyújtjuk megfelelő

méretűre. Ezért a korábbi for ciklus helyett itt egy while ciklus szerepel,

amely az előre olvasási technikát valósítja meg.

void FillVector(vector<int> &t)

{

ifstream f;

bool error = true;

string str;

do{

cout << "Fájl neve: ";

cin >> str;

f.open( str.c_str() );

if ( error = f.fail() ){

cout << "Nincs ilyen nevű fájl" << endl;

f.clear();

}

}while (error);

int e;

f >> e;

while(!f.eof()){

Page 234: PROGRAMOZÁSEz 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

234

t.push_back(e);

f >> e;

}

f.close();

}

Tesztelés

A Count()fekete doboz teszteléséhez a feladat érvényes teszteseteit

használjuk. Külön fehér doboz tesztelésre itt nincs szükség:

1. Üres tömb esete. (válasz: 0 darab páros szám)

2. Egyetlen páros szám a tömbben. (válasz: 1 darab páros szám)

3. Egyetlen páratlan szám a tömbben. (válasz: 0 darab páros szám)

4. Tetszőleges tömb, negatív számokkal.

5. Több elemű tömb, csak az első eleme páros. (válasz: 1 darab páros

szám)

6. Több elemű tömb, csak az utolsó eleme páros. (válasz: 1 darab páros

szám)

A FillVector()egy érvényes bemeneti tömböt állít elő, amely a

feladat szövege szerint garantált. Nincsenek tehát a feladatnak vizsgálandó

érvénytelen tesztesetei. Fehér doboz tesztesetek azonban vannak:

1. Nem létező állománynév esete.

2. Egymás után több rossz állománynév beírása.

3. Különféle elválasztó jelek alkalmazása a szöveges állományban.

4. Nulla, egy, sok elemű tömb előállítása.

A main() fekete doboz tesztelésénél a feladat érvénytelen tesztadatait

kell kipróbálni, de ez itt már nem vezet újabb tesztesetekhez.

Page 235: PROGRAMOZÁSEz 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

235

Teljes program

#include <iostream>

#include <fstream>

#include <vector>

#include <string>

using namespace std;

void FillVector(vector<int> &t);

int Count(const vector<int> &t);

int main()

{

cout << "Megszámoljuk a páros számokat.\n";

vector<int> t;

FillVector(t);

cout << "A páros elemek száma: " << Count(t);

return 0;

}

int Count(const vector<int> &t)

{

int s = 0;

for(int i = 0; i<(int)t.size(); ++i){

if(t[i]%2 == 0) ++s;

}

Page 236: PROGRAMOZÁSEz 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

236

return s;

}

void FillVector(vector<int> &t)

{

ifstream f;

bool error = true;

string str;

do{

cout << "Fájl neve: ";

cin >> str;

f.open( str.c_str() );

if ( error = f.fail() ){

cout << "Nincs ilyen nevű fájl" << endl;

f.clear();

}

}while (error);

int e; f >> e;

while(!f.eof()){

t.push_back(e); f >> e;

}

f.close();

}

C++ kislexikon

Page 237: PROGRAMOZÁSEz 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

237

eljárás-alprogram void Name()

{

}

eljárás hívása Name();

függvény-alprogram int Name()

{

int b = …;

return 5*b;

}

függvény hívása int a = Name();

kizárólag bemenő

paraméterváltozók

void Name(const string &str, int k)

{

}

bemenő és

eredmény-

paraméterváltozó

void Name(int &k, bool &l)

{

}

vegyes int Name(int k, bool &l)

{

l = true;

Page 238: PROGRAMOZÁSEz 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

238

return 5*k;

}

Page 239: PROGRAMOZÁSEz 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

239

7. Programozási tételek implementálása

A visszavezetésre (lásd első kötet) támaszkodó programtervezés eredménye

egy olyan program, amelyben önálló egységeket alkotnak a programozási

tételekből származtatott programrészek. Egy-egy ilyen programrész

alprogramba szervezhető, és a kódja szabványosítható, azaz kis

odafigyeléssel elérhető, hogy ugyanannak a programozási tételnek a kódja

mindig ugyanúgy nézzen ki.

Implementációs stratégia

A visszavezetés technikájával tervezett programok különféle programozási

tételek mintája szerint épülnek fel, és jól elkülöníthetők bennük az egyes

tételekből származtatott részek. Mivel ezek a részek egy-egy részfeladatot

oldanak meg, ezért mindenképpen érdemes a kódjaikat külön

alprogramokban megadni. Ennek megfelelően a kódban egy programozási

tétel egy alprogram lesz.

Mivel egy programozási tétel egyetlen ciklust tartalmaz, ez az

implementációs stratégia lényegében az egy alprogram-egy ciklus elvét is

kimondja. Természetesen ezt az elvet rugalmasan kell kezelnünk, mert

például egy mátrixban történő számlálásnál, amelynél az elemek bejárását

egy dupla ciklussal végezzük, nem lenne célszerű a belső ciklust külön

alprogramba tenni. Lehetnek olyan esetek is, amikor „több tételnyi kódot”

tartalmaz egyetlen függvény, amely esetleg kiegészül más funkciójú

kódrészekkel, például egy beolvasással.

A programozási tételekből származtatott algoritmusok kódolására

érdemes kialakítani egy olyan stílust, amelyet programjainkban egyfajta

szabványként alkalmazunk. Amilyen következetesen járunk el a visszavezetés

tervezési módszerének alkalmazásában, éppen olyan következetesen

ragaszkodjunk a kialakított kódolási stílushoz. Miért is kellene, mondjuk egy

számlálást mindig másképpen kódolni? Vizsgáljuk meg a választott

programozási nyelv nyújtotta lehetőségeket, válasszuk ki a lehető

legáltalánosabb, futási idejében leggyorsabb kódot, és azt minden

körülmények között következetesen, mint egy kódmintát, alkalmazzuk.

Page 240: PROGRAMOZÁSEz 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

240

Az ilyen önként vállalt vagy a munkahelyünkön kialakított

megállapodására épülő szabvány alkalmazása a kezdeti nehézségeket

leszámítva meggyorsítja a kódolást, jelentősen csökkenti a kódolás során

elkövetett hibákat. A kódunk áttekinthetővé, mások számára is jól

olvashatóvá válik. Ez utóbbi szempont miatt célszerű, ha nem egyedi stílust

alakítunk ki, hanem a programozó szakmában kialakított sémákat utánozzuk.

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.

Összegzés

Számlálás

Maximum kiválasztás

Lineáris keresés

Kiválasztás

Feltételes maximumkeresés

Page 241: PROGRAMOZÁSEz 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

241

Bár nem az implementációs stratégiákhoz tartozik, de itt érdemes

néhány szót szólni a visszavetetéssel készült programok tesztelési

stratégiájáról. Amikor egy programrész valamely intervallumra

megfogalmazott programozási tétel mintájára készül, akkor az alábbi

(érvényes) teszteset-csoportokat érdemes vizsgálni.

Az intervallum tesztelése olyan teszteseteket foglal magába, amikor

megvizsgáljuk a megoldó programot üres intervallumra (ennek maximum

kiválasztás esetén nincs értelme); egy elemű intervallumra; és olyan

adatokkal, amelyekből kiderül, hogy az intervallum eleje is, vége is vizsgálat

alá esett. Ez utóbbi egy számlálásnál azt kívánja, hogy a feltétel az

intervallum első és utolsó elemére is igaz legyen. Egy maximum

kiválasztásnál azt, hogy a maximális elem az első vagy az utolsó helyen

jelenjen meg. Lineáris keresésnél azt, hogy a vizsgált tulajdonság az első vagy

csak az utolsó helyen legyen igaz.

A programozási tétel tesztelése az alkalmazott algoritmus minta

specialitásait vizsgálja. Számlálásnál azt, hogy a válasz lehet-e 0, 1 illetve

több. Maximum kiválasztásnál azt, hogy megtalálja-e az egyetlen, vagy több

közül az egyik maximális elemet. Lineáris keresésnél olyan esetet nézünk,

amikor nincs keresett elem, amikor van, és a ha több is van, akkor vajon az

elsőt találjuk-e meg.

Az elemek típusának tesztelése a programozási tételekben szereplő

intervallumon értelmezett függvény értékkészletének illetve az

intervallumon értelmezett feltétel kiszámításában résztvevő értékek

típusérték halmazának különleges, szélsőséges elemeivel számol. Például ott,

ahol egész számokkal van dolgunk, érdemes a nullával, az eggyel, negatív és

pozitív számokkal is kipróbálni a programot. Ha sztringekkel dolgozunk, akkor

az üres sztring, az ékezetes betűket tartalmazó sztring számít különleges

értéknek.

Látjuk, hogy a fenti teszt-csoportok mindegyikéhez több teszteset is

tartozik. Minden tesztesethez a megnevezésén túl készíteni kell egy

tesztadatot, valamint az arra várt helyes választ, és ezekkel a tesztadatokkal

kell letesztelni a programunkat.

Page 242: PROGRAMOZÁSEz 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

242

Nyelvi elemek

Egy programozási tételt sokféleképpen kódolhatunk egy programozási

nyelven. C++ nyelven például lehet a ciklust while vagy for utasítással

kódolni, eldönthetjük, hogy akarunk-e speciális ugró utasításokat (break,

continue) alkalmazni. Az is tőlünk függ, hogy egy alprogramba ágyazott

kódrészlet eredménye milyen módon (eredmény-paraméter, visszatérési

érték) jusson vissza a hívás helyére. Ezekre a kérdésekre elég egy, lehetőleg

általános és hatékony megoldást megadni, és aztán ehhez a

megállapodáshoz igazodva kis gyakorlással elérhető legyen, hogy egyazon

tételből származó kódot mindig ugyanúgy írjunk le.

Például a számlálásra mindkét alábbi változat jó, de eldöntjük, hogy

ezen túl következetesen mindig az elsőt fogjuk használni.

int s = 0; int s = 0, i = m;

for(int i=m; i<=n; ++i){ while( i<=n ){

if( felt(i)) ++s; if( felt(i)) ++s;

} ++i;

}

A stílus uniformizálásának egyik alapja lehet a számlálós ciklus

következetes alkalmazása, mivel a tételek többsége előre meghatározott

számú iterációt végez. Ez alól a lineáris keresés és a kiválasztás programozási

tétele kivétel, mivel ott a ciklus leállása egy speciális feltételtől függ.

Egyébként pont a lineáris keresés az, amelyet meglehetősen sokféleképpen

lehet kódolni. Ennek egyik oka az, hogy már a programozási tételben

szereplő absztrakt algoritmus leírására is több változat létezik, vannak

speciális változatok, de az általános változatnak is több lehetséges leírása. (A

mi változatunkat az első kötetben találjuk.) A másik ok az, hogy azokban a

programozási nyelvekben (pl. Pascal), ahol a for ciklus kizárólag a számlálós

ciklusok kódolására szolgál a lineáris keresést csak elől tesztelős (while)

ciklussal tudjuk kódolni. (Hagyjuk figyelmen kívül a ciklusmagból „goto”

segítségével történő kiugrás lehetőségeit.) A C-szerű nyelvekben azonban a

Page 243: PROGRAMOZÁSEz 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

243

for utasítás általánosabb, mint egy számlálós ciklus, és ezért ott lehetőség

nyílik a korábban említett egységes kódolás érdekében a lineáris keresés

kódolásához is for ciklust használni. Mivel azonban a for utasítás igen

rugalmas, ezért ezzel is többféle verzió készíthető.

Nézzünk két, for utasítást tartalmazó megoldást (ebben a felt(i)

egy itt nem meghatározott, a keresés feltételét vizsgáló logikai értékű

függvény). Mindkettő a lehető legáltalánosabb megoldást nyújtja, de a

jobboldali egy speciális ugró utasítást (break) is tartalmaz. Ennek

végrehajtásakor a vezérlés kiugrik a ciklusból az azt követő utasításhoz.

bool l = false; bool l = false;

int ind; int ind;

for(int i=m; !l && i<=n; ++i){ for(ind=m; ind<=n; ++ind){

l = felt(i); if(l= felt(ind)) break;

ind = i; }

}

A jobboldali változat talán furcsa a strukturált programozáshoz

végletekig ragaszkodó programozók számára, de az előnye kézenfekvő: míg

baloldali változatban egy külön lokális ciklusváltozó futja be az m..n

intervallumot és az esetlegesen megtalált elem egy másik (ind) változóba

kerül, addig jobboldali változatban az ind változó egyszerre ciklusváltozó és

eredményváltozó is. Ha megszokjuk, hogy kizárólag egy lineáris keresés

kódolásánál használjuk a for ciklust a break utasítással együtt a fenti

formában, akkor ez a fajta strukturálatlanság egyáltalán nem fog problémát

okozni. (Természetesen az ugró utasítások nyakra-főre alkalmazását kerülni

kell, azokat csak jól bejáratott szövegkörnyezetben, valamilyen strukturált

program rövidített leírásaként szabad használni.)

A kiválasztásnál még intervallumról sem beszélhetünk, amelynek

elemeit be kellene futni, mégis alkalmazható a kódolásához a for utasítás.

Arra kell csak figyelni, hogy a ciklusváltozó itt egyben eredményváltozó is,

Page 244: PROGRAMOZÁSEz 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

244

ezért nem a for utasítás hatáskörében kell definiálni azt, hanem még a ciklus

előtt:

int i;

for(i = m; !felt(i); ++i);

A feltételes maximumkeresés (egy f(i) függvénykifejezés értékei

között keresünk felt(i) feltételt kielégítő legnagyobb értéket) kódjában

felhasználhatjuk a C++ nyelv continue ugró utasítását. Ennek hatására a

vezérlés a ciklusmag többi részét átugorja, és a ciklusfeltétel kiértékelésénél

folytatódik. Olyan ez, mintha a ciklusmag többi része a continue-t

tartalmazó if ágnak az else ágában lenne. Ha úgy tekintünk a continue

szerepére, hogy az egy elágazás egyszerűsített formájú leírására szolgál, mert

segítségével nem kell túlságosan egymásba ágyazni a ciklusmag elágazásait,

akkor ezt már a strukturált programozás hívei is használhatják.

bool l = false;

for(int i=0; i<n; ++i){

if (!felt(i)) continue;

if(l){

if(f(i)>max){

max = f(i); ind = i;

}

}else {

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:

Page 245: PROGRAMOZÁSEz 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

245

bool l = false;

for(int i=0; i<n; ++i){

if (felt(i) && l){

if(f(i)>max){

max = f(i); ind = i;

}

}else if (felt(i) && !l){ {

l = true; max = f(i); ind = i;

}

}

Szabványosítást igényel egy-egy programozási tételt tartalmazó

alprogram paraméterezésének módja is. Első pillantásra célravezetőnek

tűnik, ha az eredményt visszatérési értékként adjuk vissza, de ebben erős

korlátozást jelenthet a választott programozási nyelv. Szerencsére a C++

nyelv ebből a szempontból is rugalmas: igaz, hogy csak egy értéket tud egy

függvény visszaadni, de annak tetszőleges lehet a típusa. Ha egyszerre több

eredménye is van egy függvénynek, akkor azokat egy struktúrába összefogva

egy összetett értékként tudjuk visszaadni a hívás helyére. Például a feltételes

maximumkeresésnél az alábbi megoldást választhatjuk (Konkrét esetben a

Value helyére majd a vizsgált elemek olyan típusát kell írni, amelyre

értelmezettek az összehasonlító operátorok.)

struct result{

bool l; // van-e keresett elem

int ind; // legnagyobb keresett elem indexe

Value max; // legnagyobb keresett elem

};

Page 246: PROGRAMOZÁSEz 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

246

result feltmaxker(…);

Mégsem ezt tartjuk a legjobb megoldásnak. Ha több eredménye is van

egy tételnek, amelyek között van egy logikai érték (ilyen a feltételes

maximumkeresés és a lineáris keresés), akkor szerencsésebb, ha csak a

logikai értéket adja vissza visszatérési értékként az alprogram, a többi

eredményt pedig eredmény paraméterváltozó segítségével. Ennek a

használata kényelmesebb, mint az előbb bemutatott forma (ezt a

továbbiakban érintett feladatok megoldása támasztja majd alá), ráadásul a

korlátozottabb lehetőségeket biztosító nyelvek (pl. Pascal) esetében is

alkalmazható.

bool feltmaxker( … , int &ind, Value &max);

bool linker( … ,int &ind);

Page 247: PROGRAMOZÁSEz 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

247

16. Feladat: Legnagyobb osztó

Határozzuk meg az egynél nagyobb természetes szám önmagától különböző

legnagyobb osztóját.

Specifikáció

A feladat könnyen megoldható, ha a megadott szám felétől elindulunk lefelé

az egész számokon, és az első olyat keressük, amelyik osztja a megadott

számot. Ilyen osztó biztos van, ha más nem, akkor az 1. Ez tehát egy

„biztosan fog találni” jellegű keresés, azaz egy kiválasztás, amelyet nem a

szokásos módon, hanem a számegyenesen lefelé haladva kell alkalmaznunk.

A = ( n : ℕ, d : ℕ )

Ef = ( n=n’ n>1 )

Uf = ( n=n’ nddnd

select2/

)

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

ksearch

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.

Page 248: PROGRAMOZÁSEz 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

248

d := n/2

d := d - 1

A második esetben a lineáris keresés programozási tételére

vezethetjük vissza a megoldásnak azt a részét, amelyik a legkisebb valódi

osztót (k) keresi. Ezt követően még egy elágazásra is szükség van, amely ha a

keresés eredményes volt, akkor n/k alakban kiszámolja a legnagyobb n-től

különböző osztót, különben 1-ként adja meg azt.

,i := hamis,2

i≤√

:=

k := i

i := i + 1

l

d := n/k d := 1

Implementálás

A program három részre tagolódik: az adott szám beolvasására, a legnagyobb

nem triviális osztó keresésére és az eredmény kiírására. Ezt a három lépést

ciklikusan, újra és újra végre lehet majd hajtatni.

A main függvényen kívül két függvényt vezetünk be. Az egyik

(ReadInt) egész számok biztonságos beolvasását végzi majd, amelynek meg

lehet paraméterként adni azt, hogy csak az egynél nagyobb egész számokat

fogadja el. A másik a legnagyobb osztó kereséséért (Divisor) felel.

A tervezett ReadInt()-hez hasonló függvényekkel már találkoztunk.

(Lásd előző fejezet ReadNat() vagy ReadNotNullInt() függvényeit.)

Page 249: PROGRAMOZÁSEz 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

249

Ezeket most úgy általánosítjuk, hogy speciális paramétereként meg lehessen

neki adni egy olyan ellenőrző függvényt, amely egész számot vár

bemenetként és egy logikai értéket ad vissza. Ez most a GreaterThanOne()

lesz, amelyik eldönti egy egész számról, hogy az 1-nél nagyobb-e. Ha ez a

feltétel nem teljesül, akkor a beolvasás a megadott számot nem fogadja el.

A Divisor() függvény bemenő adatként kap egy 1-nél nagyobb

természetes számot, visszatérési értékként pedig ennek önmagától

különböző legnagyobb osztóját várjuk tőle.

Függvények hívási láncolata

Jó áttekintést ad a programról az abban használt függvények hívási

kapcsolatrendszere. Ebből kiolvasható, hogy egy függvény melyik másik

függvényt hívja meg. Ezeket a hívási láncokat mutatja az alábbi ábra.

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.

main()

ReadInt() GreaterThanOne()

Divisor()

Page 250: PROGRAMOZÁSEz 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

250

Page 251: PROGRAMOZÁSEz 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

251

Program keret

A program main függvény előtt találjuk a programban használt függvények

deklarációit. Az All() függvény szerepére később mutatunk rá.

#include <iostream>

using namespace std;

bool GreaterThanOne(int n);

bool All(int n);

int ReadInt(const string &msg, const string &errmsg,

bool check(int) = All));

int Divisor(int n);

A program main függvénye tartalmazza a futtatás végtelenítését

biztosító do-while utasítást, amelynek a törzsében a szám beolvasását végző

ReadInt() függvény meghívására és a számolást végző Divisor()

függvény eredményének kiírására kerül sor.

int main()

{

cout << "Legnagyobb osztó!\n";

char ch;

string tmp;

do{

int n = ReadInt("A szám: ",

"1-nél nagyobb szám kell!",

Page 252: PROGRAMOZÁSEz 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

252

GreaterThanOne);

cout << "Osztó: " << Divisor(n) << endl;

cout << "Futtassam újra? (I/N)";

cin >> ch; getline(cin, tmp);

}while( ch != 'n' && ch != 'N');

return 0;

}

A main függvényt a többi függvény definíciója követi.

Page 253: PROGRAMOZÁSEz 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

253

Beolvasás

Egy egynél nagyobb egész szám beolvasásához az előző fejezetben

bemutatott ReadInt() továbbfejlesztését, általánosítását használjuk, ahol

az ellenőrzést egy külön függvény végzi el. Erre az ellenőrző függvényre a

ReadInt() törzsében egy speciális változóval (check) hivatkozunk. Ez a

változó eddig még nem látott módon úgynevezett függvény típusú, azaz egy

függvény adható neki értékül. Nekünk most olyan ellenőrzőfüggvényre van

szükségünk, amely egy egész számot vár bemenő paraméterként és egy

logikai értéket ad vissza, tehát a paraméterváltozónk a bool check(int)

segítségével deklarálható.

Amikor a ReadInt()törzsében a check(…) kifejezés (az

argumentuma helyén egy tetszőleges egész szám értékű kifejezés,

legegyszerűbb esetben egy egész típusú változó állhat) kiértékeléséhez ér a

vezérlés, akkor ez meghívja a check változóban tárolt függvényt az

argumentumában levő kifejezés értékével, majd a függvény által visszaadott

értéket veszi fel.

Mivel a feladatban egynél nagyobb egész számra van szükségünk,

ezért a check változónak olyan ellenőrző függvényt kell értékül adnunk,

amelyik egy egész számra akkor ad igaz választ, ha az nagyobb, mint 1. Ilyen

függvény az alábbi:

bool GreaterThanOne(int n){ return n>1;}

Jól látható, hogy ennek a függvénynek a típusa megegyezik a

ReadInt() harmadik paraméterváltozójának, a check változónak a

típusával. A check paraméterváltozónak alapértelmezés szerinti értéket is

lehet adni. Ez arra szolgál, hogy ha a ReadInt() hívása esetén nem adnánk

meg a harmadik paramétert, akkor az alapértelmezés szerinti függvény

legyen a check -ben tárolt ellenőrző függvény. Kézenfekvő választás erre a

minden egész számot elfogadó All() függvény. Ebben az alkalmazásunkban

ugyan nem hívódik meg ez a függvény, de jelenléte lehetőséget ad a

ReadInt() későbbi általános használatához.

Page 254: PROGRAMOZÁSEz 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

254

bool All(int n) { return true;}

A ReadInt()mindhárom paraméterváltozója egy-egy hivatkozás. Az

első két esetben ezt a & jel mutatja, a harmadik pedig hivatalból az, mert ez

típusa alapján egy ellenőrző függvényre hivatkozik. Ugyanakkor mindhárom

hivatkozás szerinti paraméterváltozó kizárólag bemenő adatforgalmat

bonyolít le. Az első két esetben ezt a const kulcsszó jelzi, a harmadik

esetben csak az önmegtartóztatás, amely nem engedi meg a check változó

megváltoztatását.

int ReadInt(const string &msg, const string &errmsg,

bool check(int)= All)

{

int n;

int error = true;

string tmp;

do{

cout << msg;

cin >> n;

if(error = cin.fail() || !check(n)){

cout << errmsg << endl;

cin.clear();

}

getline(cin,tmp);

}while(error);

Page 255: PROGRAMOZÁSEz 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

255

return n;

}

A ReadInt() működése akkor fejeződik be, ha sikerül megadnia a

felhasználónak egy megfelelő egész számot (mi esetünkben egynél

nagyobbat), és ilyenkor ezzel az egész számmal tér vissza.

Page 256: PROGRAMOZÁSEz 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

256

Absztrakt program kódolása

A Divisor() függvény, attól függően, hogy melyik absztrakt

programot valósítjuk meg, vagy a kiválasztásra épülő változat kódját, vagy a

lineáris keresésre épülő változatét tartalmazza. Mindkét esetben egyetlen

bemenő paramétere lesz: a korábban beolvasott egynél nagyobb egész szám,

a visszatérési értéke pedig a megadott számnak önmagától különböző

legnagyobb osztója.

A kiválasztás:

int Divisor(int n)

{

int d;

for(d = n/2; n%d != 0; --d);

return d;

}

A keresés (ehhez szükség van a cmath csomagra):

int Divisor(int n)

{

int k;

bool l = false;

for(int i = 2; !l && i<=sqrt(n); ++i){

l = n%i == 0;

k = i;

Page 257: PROGRAMOZÁSEz 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

257

}

if(l) return n/k;

else return 1;

}

Page 258: PROGRAMOZÁSEz 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

258

Tesztelés

Modulonkénti tesztelést végzünk.

A Divisor() függvény fekete doboz teszteléséhez (a lineáris keresést

vizsgáljuk, de ennek tesztesetei a kiválasztáshoz is jók) a feladat érvényes

tesztesetei használhatóak. Külön fehér doboz tesztelésre nem lesz szükség:

1. Intervallum teszt

a. üres intervallum: 2 esetén az osztó 1.

b. egyelemű intervallum: 3 esetén az osztó 1.

c. intervallum eleje: 13 esetén az osztó 1.

d. intervallum vége: 16 esetén az osztó 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).

Page 259: PROGRAMOZÁSEz 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

259

Teljes program

Tekintsük meg a teljes programot.

#include <iostream>

#include <string>

#include <cmath>

using namespace std;

bool GreaterThanOne(int n);

bool All(int n);

int ReadInt(const string &msg, const string &errmsg,

bool check(int) = All);

int Divisor(int n);

int main()

{

cout << "Legnagyobb osztó!\n";

char ch;

string tmp;

do{

int n = ReadInt("A szám: ",

"1-nél nagyobb szám kell!",

GreaterThanOne);

Page 260: PROGRAMOZÁSEz 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

260

cout << "Osztó: " << Divisor(n) << endl;

cout << "Futtassam újra? (I/N)";

cin >> ch; getline(cin, tmp);

}while( ch != 'n' && ch != 'N');

return 0;

}

int Divisor(int n)

{

int d;

for(d = n/2; n%d != 0; --d);

return d;

}

bool All(int n) { return true;}

bool GreaterThanOne(int n){ return n>1; }

int ReadInt(const string &msg, const string &errmsg,

bool check(int))

{

Page 261: PROGRAMOZÁSEz 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

261

int n;

int error = true;

string tmp;

do{

cout << msg;

cin >> n;

if(error = cin.fail() || !check(n)){

cout << errmsg << endl;

cin.clear();

}

getline(cin,tmp);

}while(error);

return n;

}

Page 262: PROGRAMOZÁSEz 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

262

17. Feladat: Legkisebb adott tulajdonságú elem

Keressük meg egy szöveges állományból feltöltött egész értékű tömbben a

legkisebb olyan számot, amely k-val osztva 1-et ad maradékul! Az állomány

csak egész számokat tartalmaz. A legelső szám a beolvasandó számok

darabszáma, amelyet megfelelő számú (legalább darabszámnyi) egész szám

követ elválasztó jelekkel (szóköz, tabulátor-, sorvége jel) határolva.

Specifikáció

A feladat elvi megfogalmazásában egy tömb a bemenő adat, továbbá három

féle eredmény van: logikai érték, amely jelöli, hogy van-e egyáltalán a

tömbnek k-val osztva 1-et adó eleme, ha igen melyik a legkisebb, és ez

hányadik indexű helyen található.

A = ( t : ℤn, k : ℤ, l : 𝕃, min, ind : ℤ )

Ef = ( t=t’ k=k’ )

Uf = ( t=t’ ][,,

mod][

itindminln

kitimin

11

Absztrakt program

A feladatot a feltételes maximumkeresés programozási tételére vezethetjük

vissza.

l := hamis i: ℤ

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

Page 263: PROGRAMOZÁSEz 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

263

Implementálás

A tömböt vector<int> típussal definiáljuk, amelynek elemei 0-tól

indexelődnek, ezért az absztrakt algoritmus ciklusa 0..n–1 intervallumot futja

majd be.

A program most is három részre tagolódik: beolvasás, feldolgozás,

kiírás, amelyek közül az első kettőt önálló alprogramként valósítjuk meg.

Szükségünk lesz egy egész számot beolvasó függvényre és két ellenőrző

függvényre is: az egyik minden egész számot elfogad, a másik csak a nullától

különbözőeket. Ennél fogva a forrásállományban (legyen ennek a neve

main.cpp) a main függvényen kívül még öt másik függvény is megjelenik,

amelyek közül négyet a main függvény törzsében használunk.

int main()

{

vector<int> t;

Fill(t);

int k = ReadInt("A szám, amivel osztunk: ",

"Nemnulla egész szám kell!",

NotNull);

int min, ind;

if(CondMinSearch(t,k,min,ind))

cout << "A legkisebb keresett szám: " << min

<< "ami a(z) " << ind+1 << "-edik.\n";

else cout <<"Nincs keresett szám!\n";

return 0;

}

Page 264: PROGRAMOZÁSEz 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

264

Függvények hívási láncolata

A main függvény kódjából jól látható a program függvényeinek hívási

szerkezete. Ezen hívási láncok mentén történik az adatáramlás az egyes

függvények között. A main() egy tömböt kap a Fill() függvénytől, egy

nullától különböző egész számot a ReadInt()-től. A ReadInt() a

beolvasott számot a NotNull() függvénynek adja oda vizsgálatra, amelyik

egy logikai értékkel jelzi vissza annak helyességét. A CondMinSearch()

megkapja a tömböt, a nullától különböző számot, és visszaadja a minimális

adott számmal osztható legkisebb tömbelem értékét és indexét, valamint

egy logikai értéket, hogy volt-e egyáltalán adott számmal osztható elem a

tömbben.

7-3. ábra. Alprogramok hívási láncai

Beolvasás

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,

main()

Fill()

ReadInt() NotNull()

CondMinSearch()

Page 265: PROGRAMOZÁSEz 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

265

hogy az állomány megfelelően van kitöltve, csak azt vizsgáljuk, hogy az

állomány nevét jól adták-e meg.

Az osztó (k) beolvasását az előző fejezetben bevezetett ReadInt()

függvénnyel végezzük, amelynek most azt kell vizsgálnia, hogy a megadott

szám nullától különböző-e. Ehhez el kell készítenünk az alábbi ellenőrző

függvényt, amelyet a ReadInt() hívásakor annak harmadik paramétereként

kell megadnunk.

bool NotNull(int n){ return n != 1; }

Természetesen a kód tartalmazza a ReadInt() függvény definícióját

és az annak deklarációjában szereplő alapértelmezett paraméterértékként

szereplő All() függvény definícióját is.

Absztrakt program kódolása

Feltételes minimumkeresés kódja a fejezet bevezetőjében elmondottak

alapján készült.

bool CondMinSearch(const vector<int> &t,

int k, int& min, int& ind)

{

bool l = false;

for(int i=0; i<(int)t.size(); ++i){

if (t[i]%k != 1) continue;

if(l){

if(t[i]<min){ min = t[i]; ind = i;}

}else {l = true; min = t[i]; ind = i;}

}

Page 266: PROGRAMOZÁSEz 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

266

return l;

}

Tesztelés

Most is modulonkénti tesztelést végeztünk.

A CondMinSearch() teszteléséhez az érvényes tesztadatok szolgálnak

tesztesetként.

1. Intervallum teszt

a. (üres intervallum) Nulla darab szám esete: az állomány egy 0

értéket tartalmaz, a helyes válasz az, hogy nem találtunk

keresett tulajdonságú elemet.

b. (intervallum eleje) Legelöl található a legkisebb k-val osztva

az 1 maradékot adó szám.

c. (intervallum vége) Leghátul a található a legkisebb k-val

osztva az 1 maradékot adó szám.

2. Programozási tétel tesztje

a. Egyetlen, k-val osztva nem az 1 maradékot adó szám esete: a

helyes válasz az, hogy nem találtunk keresett tulajdonságú

elemet.

b. Egyetlen, k-val osztva az 1 maradékot adó szám esete: a

helyes válasz ez a szám.

c. Az legelső szám k-val osztva nem az 1-et adja maradékul, de

a k-val osztva 1 maradékot adó számok közül a legkisebb van

legelőrébb.

d. A legutolsó szám k-val osztva nem az 1-et adja maradékul, de

a k-val osztva 1 maradékot adó számok közül a legkisebb van

leghátrébb.

e. A legkisebb, k-val osztva 1 maradékot adó szám többször is

előfordul, legelöl is, leghátul is, csak középen

3. Különleges értékek tesztje

a. Negatív számok is legyenek a tömbben

Page 267: PROGRAMOZÁSEz 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

267

b. Az osztó lehet 1, páros, páratlan, prím, negatív.

Az érvénytelen tesztadatok a beolvasást végző alprogramok számára

biztosítanak teszteseteket.

A Fill() eljárásnál azonban nem kell érvénytelen adatokra számítani,

mivel a szöveges állományban megállapodás szerint helyesen van kitöltve.

Fehér doboz teszteléséhez meg kell vizsgálni az alábbiakat:

1. Rossz állomány név megadása egymás után többször is.

2. Különböző hosszú tömbök esetei.

A ReadInt()tesztelésénél egyrészt ki kell próbálni, hogy az All()

ellenőrző függvényre jól működik-e, majd hogy a NotNull() mellett is.

Fehér doboz tesztelésnél egymás után többször is érvénytelen adatot kell

megadni.

1. Egész számok (-302, -1, 0, 1, 15) beolvasása.

2. Negatív egész számok esete.

3. Nem egész szám esete.

A main() fekete doboz tesztelésénél a feladat érvénytelen tesztadatait

kellene kipróbálni, de ez itt már nem vezet újabb tesztesetekhez.

Page 268: PROGRAMOZÁSEz 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

268

Teljes program

#include <iostream>

#include <fstream>

#include <vector>

#include <string>

using namespace std;

void Fill(vector<int> &t);

bool CondMinSearch(const vector<int> &t,

int k, int &min, int &ind);

bool NotNull(int n);

bool All(int n);

int ReadInt(const string &msg, const string &errmsg,

bool check(int) = All);

int main()

{

vector<int> t;

Fill(t);

int k = ReadInt("A szám, amivel osztunk: ",

"Nemnulla egész szám kell!",

NotNull);

int min, ind;

if(CondMinSearch(t,k,min,ind))

Page 269: PROGRAMOZÁSEz 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

269

cout << "A legkisebb keresett szám: " << min

<< "ami a(z) " << ind+1 << "-edik.\n";

else cout <<"Nincs keresett szám!\n";

return 0;

}

bool CondMinSearch(const vector<int> &t,

int k, int& min, int& ind)

{

bool l = false;

for(int i=0; i<(int)t.size(); ++i){

if (t[i]<=0) continue;

if(l){

if(t[i]<min){ min = t[i]; ind = i;}

}else {l = true; min = t[i]; ind = i;}

}

return l;

}

void Fill(vector<int> &t)

{

ifstream f;

bool hiba;

string str;

do{

cout << "Fájl neve:";

Page 270: PROGRAMOZÁSEz 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

270

cin >> str;

f.open( str.c_str() );

if ( hiba = f.fail() ){

cout << "Nincs ilyen nevű fájl" << endl;

f.clear();

}

}while (hiba);

int n;

f >> n;

t.resize(n);

for(int i=0; i<n; ++i){

f >> t[i];

}

f.close();

}

bool All(int n) { return true;}

bool NotNull(int n){ return n != 1;}

int ReadInt(const string &msg, const string &errmsg,

bool check(int))

{

int n;

Page 271: PROGRAMOZÁSEz 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

271

int error = true;

string tmp;

do{

cout << msg;

cin >> n;

if(error = cin.fail() || !check(n)){

cout << errmsg << endl;

cin.clear();

}

getline(cin,tmp);

}while(error);

return n;

}

Page 272: PROGRAMOZÁSEz 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

272

18. Feladat: Keressünk Ibolyát

Egy tömb keresztneveket tartalmaz. Van-e a keresztnevek között Ibolya

illetve minden név Ibolya-e?

Specifikáció

Ez itt két különböző feladat, ezért külön-külön oldjuk meg őket. A

specifikációknak azonban csak az utófeltétele tér el egymástól.

A = ( t : String n, l : 𝕃 )

Ef = ( t=t’ )

Uf = ( t=t’ ""][ Ibolyaitln

i

search1

)

Uf = ( t=t’ ""][ Ibolyaitln

i

search

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:ℤ l, i := igaz, 1 i:ℤ

¬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

Page 273: PROGRAMOZÁSEz 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

273

elemei 0-tól indexelődnek, ezért az absztrakt algoritmus ciklusa 0..n–1

intervallumot futja majd be.

Eltér viszont a két programban a feldolgozás. Egyikben a

LinSearch(), a másikban az OptLinSearch() kap szerepet, és

természetesen különbözik az eredmény kiírását kísérő szöveg.

Az alábbiakban mindkét változatot megmutatjuk.

Az első változat main függvénye az alábbi:

int main()

{

vector<string> t;

Fill(t);

if(LinSearch(t)) cout << "Van Ibolya.\n";

else cout << "Nincs Ibolya.\n";

return 0;

}

A második változat main függvénye pedig az alábbi:

int main()

{

vector<string> t;

Fill(t);

if(OptLinSearch(t)) cout << "Minden név

Ibolya.\n";

else cout << "Ibolyától eltérő név is

van.\n";

return 0;

Page 274: PROGRAMOZÁSEz 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

274

}

Függvények hívási láncolata

A main egy tömböt kap a Fill() függvénytől. A LinSearch() vagy az

OptLinSearch() megkapja a tömböt, és visszaadja a vizsgálat eredményét

mutató logikai értéket.

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() );

main()

Fill()

LinSearch() / OptLinSearch()

Page 275: PROGRAMOZÁSEz 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

275

if ( hiba = f.fail() ){

cout << "Nincs ilyen nevű fájl" << endl;

f.clear();

}

}while (hiba);

f >> str;

while(!f.eof()){

t.push_back(str);

f >> str;

}

f.close();

}

Absztrakt program kódolása

Nem szorul különösebb magyarázatra a két változat absztrakt programjának

kódolása.

Az első változat:

bool LinSearch(const vector<string> &t)

{

bool l = false;

for(int i = 0; !l && i<(int)t.size(); ++i){

l = "Ibolya" == t[i];

}

Page 276: PROGRAMOZÁSEz 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

276

return l;

}

A második változat:

bool OptLinSearch(const vector<string> &t)

{

bool l = true;

for(int i = 0; l && i<(int)t.size(); ++i){

l = "Ibolya" == t[i];

}

return l;

}

Mindkettőt a lineáris keresés kódolásánál bevezetett minta alapján

készítjük el. Mindkettő a tömböt kapja meg bemenő adatként és egy logikai

értéket ad vissza visszatérési értékként.

Page 277: PROGRAMOZÁSEz 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

277

Tesztelés

A specifikáció alapján felírt érvényes tesztesetek (érvénytelen teszteset most

nincs):

1. Intervallum tesztje:

a. (üres intervallum) Nincs egyetlen név sem. Első változatnál

„Nincs Ibolya”, második változatnál: „Mindenki Ibolya”.

b. (intervallum eleje) Csak az első Ibolya. Első változatnál „Van

Ibolya”, második változatnál: „Van nem Ibolya”.

c. (intervallum vége) Csak az utolsó Ibolya. Első változatnál

„Van Ibolya”, második változatnál: „Van nem Ibolya”.

2. Programozási tétel tesztje:

a. Egy Ibolya név. Első változatnál „Van Ibolya”, második

változatnál: „Mindenki Ibolya”.

b. Egy nem Ibolya név. Első változatnál „Nincs Ibolya”, második

változatnál: „Van nem Ibolya”.

c. Csupa Ibolya név. Első változatnál „Van Ibolya”, második

változatnál: „Mindenki Ibolya”.

d. Csupa nem Ibolya név. Első változatnál „Nincs Ibolya”,

második változatnál: „Van nem Ibolya”.

e. Több név, Ibolya is. Első változatnál „Van Ibolya”, második

változatnál: „Van nem Ibolya”.

3. Különleges értékek tesztje: speciális sztringek a nevek helyén

További teszteseteket csak a Fill() függvény tesztelése ad.

1. Rossz állomány név megadása egymás után többször is.

2. Különböző hosszú tömbök esetei.

Page 278: PROGRAMOZÁSEz 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

278

Teljes program

Tekintsük meg egyben mind a két programot. A Fill() függvényt azonban

nem írjuk le részletesen, csak jelezzük a helyét.

#include <iostream>

#include <fstream>

#include <vector>

#include <string>

using namespace std;

void Fill(vector<string> &t);

bool LinSearch(const vector<string> &t);

int main()

{

// Adatok beolvasása

vector<string> t;

Fill(t);

// Kiértékelés

if(LinSearch(t)) cout << "Van Ibolya a

tömbben.\n";

else cout << "Nincs Ibolya a

tömbben.\n";

Page 279: PROGRAMOZÁSEz 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

279

return 0;

}

bool LinSearch(const vector<string> &t)

{

bool l = false;

for(int i = 0; !l && i<(int)t.size(); ++i){

l = "Ibolya" == t[i];

}

return l;

}

void Fill(vector<string> &t)

{

...

}

Page 280: PROGRAMOZÁSEz 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

280

A második program:

#include <iostream>

#include <fstream>

#include <vector>

#include <string>

using namespace std;

void Fill(vector<string> &t);

bool OptLinSearch(const vector<string> &t);

int main()

{

// Adatok beolvasása

vector<string> t;

Fill(t);

// Kiértékelés

if(OptLinSearch(t)) cout << "Minden név

Ibolya.\n";

else cout << "Van Ibolyától eltérő név is.\n";

return 0;

}

bool OptLinSearch(const vector<string> &t)

Page 281: PROGRAMOZÁSEz 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

281

{

bool l = true;

for(int i = 0; l && i<(int)t.size(); ++i){

l = "Ibolya" == t[i];

}

return l;

}

void Fill(vector<string> &t)

{

...

}

Page 282: PROGRAMOZÁSEz 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

282

C++ kislexikon

összegzés int s = 0;

for(int i=m; i<=n; ++i) s = s + f(i);

számlálás int c = 0;

for(int i=m; i<=n; ++i){

if( felt(i)) ++c;

}

maximum

kiválasztás

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 int i;

for(i = 0; !felt(i); ++i);

lineáris keresés 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);

}

Page 283: PROGRAMOZÁSEz 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

283

feltételes

maximumkeresés

Value max;

int ind;

bool l = false;

for(int i=m; i<=n; ++i){

if (!felt(i)) continue;

if(l){

if(t[i]>max){

max = t[i]; ind = i;

}

}else {

l = true; max = t[i]; ind = i;

}

}

Page 284: PROGRAMOZÁSEz 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

284

struktúra struct rekord{

bool l;

int i, ind;

bool u;

};

record r;

r.l = true;

int j = r.ind;

függvény-típusú

paraméter

void Name(bool fv(int, const string&))

Page 285: PROGRAMOZÁSEz 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

285

8. Többszörös visszavezetés alprogramokkal

A programtervezés során általában is, de ebben a könyvben különösen

erősen építünk a nevezetes algoritmus mintákra, a programozási tételekre,

amelyekből visszavezetéssel származtatjuk a programjainkat. Már az előző

fejezetben is ilyen programok implementálásával foglalkoztunk, de ott egy

feladat megoldásához egyetlen programozási tételre volt csak szükség. Most

viszont azt vizsgáljuk meg, hogyan érdemes összetett, több egymásba

ágyazott programozási tételre támaszkodó absztrakt programot kódolni.

Egy absztrakt programtervben sokszor találkozhatunk olyan (nem-

megengedett) é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

Page 286: PROGRAMOZÁSEz 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

286

előfordulhat a nullával való osztás, sőt az is, hogy ilyenkor nem számot ad

meg a felhasználó. A program ekkor sem „szállhat el”, a hibát észre kell venni

és annak okáról a felhasználót megfelelően kell tájékoztatni. Az ilyen

vizsgálatok következtében elágazások, ciklusok tucatjai épülhetnek be a

kódba, amelynek szerkezete ettől egyre összetettebb lesz. Sokkal

átláthatóbbá válik a kód azonban akkor, ha minden, a tervezésnél kizárt (nem

várt, hibás) esemény bekövetkezése esetén a működést külön „mederbe”

tereljük, amelyet olyan kódrésszel írunk le, amelyet a program többi részétől

elkülönítünk. Ezt a kódolási stílust hívjuk kivételkezelésnek.

Implementációs stratégia

Egy változó deklarációjának helyét úgy választjuk meg, hogy a láthatósága ne

terjedjen túl azon a kódrészen, ahol valóban használni akarjuk. Például egy

for ciklus úgynevezett indexváltozóját – ha tehetjük – a for ciklusban

lokálisan deklaráljuk. Így járjunk el akkor is, ha egymás után több, ugyanolyan

nevű, de egymástól nyilvánvalóan nem függő indexváltozót használó for

ciklust készítünk. Ilyenkor minden egyes ciklusra külön-külön deklarálunk egy

indexváltozót, ezáltal a ciklusok egymástól való függése csökken.

Ha a programterv tartalmaz olyan alprogramokat, amelyek lokális

változókat vezetnek be, akkor ezek a változók a megvalósított kódban is

legyenek lokálisak az alprogramra nézve. Ugyanez igaz a tervben

paraméterváltozóként feltüntetett lokális változókra is; azok legyenek

paraméterváltozók a kódban is. Ettől legfeljebb csak akkor térjünk el, hogy

például az alprogramot a tervvel szemben nem eljárásként, hanem

függvényként akarjuk használni, és ezért néhány eredmény

paraméterváltozó helyett a visszatérési érték fogja az eredményt átadni.

Ekkor az eredmény paraméterváltozó közönséges lokális változó lesz,

amelynek az értékét majd egy return utasítás keretében kell visszaadni.

Általános szabály, hogy ha egy alprogramot függvényszerűen hívunk

meg, akkor lehetőleg ne legyenek eredmény-paraméterváltozói, azaz

ilyenkor a paraméterek csak a bemenő értékeket közvetítsék. Ettől a

szabálytól természetesen indokolt esetben el lehet térni (amennyiben a

programozási nyelv lehetővé teszi). Ilyen indok lehet, a programozási tételek

kódolásánál kialakult hagyomány. Például egy lineáris keresést, amelynek

Page 287: PROGRAMOZÁSEz 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

287

több eredménye is van (talált-e keresett elemet illetve mi a keresett elem)

érdemes úgy kódolni, hogy a találat sikerét jelző logikai értéket visszatérési

értékként, a megtalált elemet paraméterként adjuk vissza. (Elterjedt az

megoldás is, amelyik csak a keresett elemet adja vissza, sikertelen keresés

esetén egy speciális, úgynevezett extremális értékkel.)

Előfordulhat olyan eset, hogy egy alprogram többször is meghívódik,

és a működése egy változójának az alprogram előző hívása során előállított

értékétől is függ. A tervben ez a változó nyilván globális az alprogramra

nézve, és még az alprogram hívásai előtt létre lett hozva. A kódban sem

szabad a változót az alprogram lokális változójaként felvenni, de globális

változóként való létrehozásával kapcsolatban erős ellenérvek vannak.

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

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.

Page 288: PROGRAMOZÁSEz 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

288

értelmezésnél. Egyfelől beszélhetünk abszolút globális változóról, amely

valóban a teljes programban látható (ilyet nem is olyan könnyű deklarálni,

csak bizonyos helyeken és formában deklarált változók lehetnek a teljes

programra nézve globálisak), de beszélhetünk egy alprogram szempontjából

vett, azaz relatív globális változóról, amelyet nem az alprogramban

deklarálunk, de ott látható és használható.

Az abszolút globális változók használata erősen ellenjavallott. Nagyon

indokolt esetben, kevés számú változó számára megengedhetjük, hogy ha

egy változót több alprogram is használ, akkor az legyen ezekre nézve globális.

Egy nagyméretű program esetében viszont az szinte kizárt, hogy olyan

változónk legyenek, amelyet a program minden részében használni kell, azaz

amelyet indokolt általánosan globálisnak választani. Szerencsére egy

kiterjedt, több forrásállományra tördelt program esetében nem is olyan

egyszerű abszolút globális változót deklarálni a programozási nyelvekben.

A csak néhány alprogramra nézve globális változók bevezetése esetén

– ha mégis ehhez folyamodnánk – pontosan kell dokumentálni, hogy melyik

alprogram olvassa, melyik írhatja felül ennek a változónak az értékét. Az ilyen

relatív globális változók deklarálásának módja erősen eltér a különböző

programozási nyelvekben.

Egy igazi blokkstrukturált nyelvben (ilyen például a Pascal nyelv) az

alprogramokat egymásba ágyazhatjuk. Ilyenkor a belső alprogram

használhatja az őt tartalmazó alprogramok lokális változóit, feltéve, hogy

ugyanolyan névvel nem vezetünk be új változókat. A beágyazó alprogram

lokális változója tehát globális változóként jelenik meg a beágyazott

alprogramban.

A C++ nyelvben viszont az alprogramok nem ágyazhatóak egymásba,

egymás után definiálhatjuk csak őket. Ezek az alprogramok hívhatják ugyan

egymást, de nem látják, nem használhatják egymás lokális változóit.

Lehetőség van ugyanakkor az alprogramokon kívül változókat definiálni, de

ilyenkor ezek az összes alprogramra nézve lesznek globálisak, hacsak nem

vetünk be egyéb, a láthatóságot korlátozó nyelvi elemet (névtér, önálló

fordítási egység, osztály).

Sose felejtsük el, hogy a globális változók használata rontja a program

átláthatóságát, növeli a rejtett hibák bekövetkezésének lehetőségét, mert

Page 289: PROGRAMOZÁSEz 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

289

sérti a lokalitás elvét, nevezetes azt, hogy egy adott programrészről

önmagában, a környezetének ismerete nélkül lehessen látni, milyen

adatokból milyen eredmény állít elő.

Mit tegyünk akkor, ha a programterv globális változót használ egy

alprogramban, de a megvalósításban ezt ki szeretnénk küszöbölni? Ennek

legegyszerűbb módja az, ha a kérdéses változót az alprogramot hívó

környezet lokális változójává tesszük, és paraméterként adjuk át az

alprogram egy erre a célra bevezetett új paraméterváltozójának. Ez az

adatcsere irányától függően lehet bemenő- vagy eredmény

paraméterváltozó, esetleg mindkettő egyszerre. Ha az alprogram csak

megváltoztatja a kiküszöbölendő globális változó értékét, de nem használja

fel bemeneti adatként, akkor az alprogramot olyan függvényként is meglehet

valósítani, amely a kérdéses változó számára állít elő új értéket, azaz a hívó

környezet szóban forgó változója a függvényhívást tartalmazó értékadás

baloldalán jelenik meg.

Kivételnek a nem várt, vagy a megoldandó feladat szempontjából

marginális eseményt nevezzük. Ilyen lehet például egy olyan hiba, amelyet

egy érvénytelen teszteset, azaz a specifikáció előfeltételében kizárt adat

okoz. A programterv nem számol az ilyen esetekkel, de az implementációnál

az a cél, hogy ne következhessen be a program futása során olyan esemény,

amelyre nincs program által leírt válasz.

A programozónak azt kell eldöntenie, hogy egy kivétel bekövetkezését

eleve elkerülje-e, vagy a bekövetkezés esetén hajtson végre egy speciális

tevékenységet a programja. Ha például el akarjuk kerülni egy bemenő adattal

történő nullával való osztást, akkor célszerű nem megvárni az osztáskor

bekövetkező hibát, és azt kezelni, hanem jobb megelőzni az osztó

beolvasásakor végzett ellenőrzéssel (elágazással vagy hátul-tesztelő

ciklussal). Azt a hibát viszont, ha egy nem létező állományt akarunk

megnyitni, sokszor éppen azáltal fedjük fel, hogy megkíséreljük a nyitást. A

fájlnyitási hiba bekövetkezésének lekezelésére ilyenkor megfelelő kódot

lehet készíteni. Ez történhet helyben (elágazással vagy hátul-tesztelő

ciklussal) vagy – amennyiben a nyelv támogatja – kivételkezeléssel, amikor a

program végrehajtását mintegy annak strukturált szerkezetéből kilépve,

speciális helyen leírt kódra bízzuk. Ennek befejeződésekor a vezérlés meg is

Page 290: PROGRAMOZÁSEz 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

290

állhat, de vissza is terelhető a normális működést leíró kódra. Habár nyelvi

szempontból csak ez utolsó, speciális nyelvi elemet igénylő megoldást szokás

kivételkezelésnek nevezni, fogalmilag minden olyan implementációs

megoldás idesorolható, amely a kivételes működés leírására szolgál.

Nyelvi elemek

A több programrészből álló alkalmazások implementálásánál fontos

pontosan ismerni, hogy a választott programozási nyelvben milyen

láthatósági szabályok vannak, hogyan lehet globális és lokális változókat

használni. Ehhez meg kell ismernünk, hogy milyen szerkezetű programok

készíthetők az adott nyelven és ez a szerkezet milyen hatással van a változók

globális és lokális láthatóságának megválasztására. Sok nyelvben

utasításblokkokat lehet kijelölni a láthatóság korlátozására, és egy ilyen blokk

szempontjából lehet beszélni egy változó lokális vagy globális láthatóságáról.

A Pascal nyelvben csak az alprogramok képeznek önálló utasításblokkot, de

ezeket egymásba lehet ágyazni. A C++ nyelven az alprogram blokkok nem

ágyazhatók egymásba, ellenben egyéb láthatóságot korlátozó

utasításblokkok (for ciklus indexváltozója, kapcsos zárójelek közé zárt részek)

is létrehozhatók.

A C++ nyelvbeli globális változók deklarálásának módozatait nem

mutatjuk be, mert a paraméterátadásra épülő adatcserének alkalmazását

részesítjük előnyben. Ennek nyelvi eszközeit az előző két fejezetben viszont

már megismertük.

Ha a kivételkezelésen olyan programrészek kódolását értjük, amelyek

végrehajtásához a program strukturált szerkezetéből történő kilépésre van

szükség, akkor az a legfontosabb kérdés, hogy a választott programozási

nyelv milyen nyelvi elemekkel támogatja ennek megvalósítását. Akár már egy

fegyelmezetten használt goto utasítás is lehet a kivételkezelés eszköze, de

jobban örülünk, ha erre külön nyelvi elemek állnak rendelkezésünkre.

A kivételkezelésnek két lényeges pontja van. Az egyik a kivétel

keletkezésének, a kivétel eldobásának a helye, amikor a program normális

vezérlése megszakad, a vezérlés „kiugrik a kódból”. A másik a kivétel

kezelése, a kivétel elkapásának helye, ahol a vezérlés egy speciális

Page 291: PROGRAMOZÁSEz 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

291

tevékenység végrehajtásával folytatódik, majd visszatér a program normális

vezérléséhez. Ha egy kivételt nem kapunk el, akkor a program abortál.

Egy program működésében lehetnek előre definiált kivételek (nullával

való osztás, index túlcsordulás, stb.) de a programozó által definiált

úgynevezett felhasználói kivételek is. Az előbbi esetben a kivétel dobása

automatikusan történik, az utóbbi esetben explicit módon kell azt

kikényszeríteni (throw). (Általában lehetőség van előre definiált kivételek

felhasználó által kényszerített kiváltására is.) A kivételek speciális értékek,

objektumok, amelyeknek van típusa, értéke, és ennél fogva képesek a kiváltó

okra vonatkozó információt eltárolni.

Kivételt csak a kód azon részében tudjuk elkapni és kezelni, amelyik

kódrészt speciális módon megjelöltünk (megfigyelt szakasz, try-blokk).

Miután azt a kód szakaszt, ahol kivétel keletkezésre számítunk, megjelöltük,

ehhez a szakaszhoz úgynevezett kivételkezelő ágakat (kezelő ág, catch-blokk)

rendelhetünk, amelyek mindegyike egy bizonyos kivételtípussal foglalkozik:

meghatározott típusú kivétel bekövetkezése esetén az ahhoz tartozó

kivételkezelő ág fog végrehajtódni. Ha a kivételkezelés mást nem mond (nem

terminálja a programot, nem dob egy másik kivételt), akkor a vezérlés a

megfigyelt kódszakasz utáni utasításon folytatódik tovább. Bizonyos

nyelvekben olyan kódrészt (lezáró szakasz, finally-blokk) is

hozzárendelhetünk egy megfigyelt szakaszhoz, amelynek végrehajtására

mindenképpen sor kerül, akkor is, ha nem következett be kivétel a megfigyelt

szakaszon, és akkor is, ha kivételkezelésre került sor.

Page 292: PROGRAMOZÁSEz 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

292

19. Feladat: Kitűnő tanuló

Egy iskola egyik n diákot számláló osztályában m különböző tantárgyból

osztályoztak a félév végén. A jegyek egy táblázat formájában

rendelkezésünkre állnak. (A diákokat és a tantárgyakat most sorszámukkal

azonosítjuk.) Állapítsuk meg, van-e olyan diák, akinek csupa ötöse van!

Specifikáció

Ennek a feladatnak a megoldását az első kötetben már megterveztük.

A = ( napló:ℕn×m , van:𝕃 )

Ef = ( napló = napló’ )

Uf = ( napló = napló’ van = i[1..n]: színjeles(i) )

ahol színjeles : [1..n]𝕃 színjeles(i) =j[1..m]: napló[i,j]=5

Absztrakt program

Az absztrakt program két egymásba ágyazott lineáris keresés.

van,i := hamis,1 i:ℕ

van in

van := színjeles(i)

i := i+1

van:=színjeles(i)

van,j := igaz,1 j:ℕ

van jm

van := napló[i,j]=5

j := j+1

Page 293: PROGRAMOZÁSEz 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

293

Az absztrakt program második részét (alsó szint) tekinthetjük egy közönséges

részprogramnak, de alprogramként is felfogható. Előbbi esetben a

részprogramot egyszerűen behelyettesítjük a felső szint van:=színjeles(i)

nem-megengedett értékadás helyébe, utóbbiban alprogram-hívásként

tekintünk erre az értékadásra, amely a végrehajtása során átadja a vezérlést

az alprogramnak. Ha a második változat mellett döntünk, akkor azt a

mátrixot, amelyre a fenti tervben globális változóként hivatkozik az

alprogram, elérhetővé kell tenni az alprogram kódjában, és arról is

döntenünk kell, hogy az alprogramot függvényszerű vagy eljárásszerű

hívással aktivizáljuk-e. Ezek olyan nyitott kérdések, amelyeket az

implementáció során kell megválaszolnunk.

Implementálás

A megoldó programban külön függvényben valósítjuk meg az absztrakt

program alprogramját, az osztályzási napló feltöltését, valamint ez utóbbi

által hívott egy egész szám beolvasását végző függvényt, amelyet többféle

ellenőrzési lehetőséggel is fel lehet ruházni. Az absztrakt algoritmus

főprogramja közvetlenül a main függvénybe kerül.

Főprogram

int main()

{

// Osztályzási napló feltöltése

vector<vector<int> > reg;

ReadMarks(reg);

// Lineáris keresés

bool exists = false;

for(int i=0; !exists && i<(int)reg.size(); ++i){

Page 294: PROGRAMOZÁSEz 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

294

exists = Excellent(reg[i]);

}

// Eredmény kiírása

if(exists) cout << "Van kitűnő diák.\n";

else cout << "Nincs kitűnő diák.\n";

return 0;

}

A főprogram három részre tagolódik. Deklarálja a feladat bemenő

(reg) és eredmény (exists) változóját, meghívja a mátrixot feltöltő

ReadMarks() eljárást, amelyet az absztrakt algoritmus főprogramja követ,

végül az eredmény kiírására kerül sor.

Absztrakt alprogram kódolása

A kitűnő diákot vizsgáló alprogramot függvényszerű hívással aktiváljuk,

az eredményt visszatérési értékként adjuk meg. Az alprogramnak nemcsak az

aktuális diák sorszáma (i) a bemenő adata (mint ahogy ezt a programterv

felületes vizsgálata sugallja), hanem szükség van az osztályzatokat tartalmazó

naplóra (reg) is. Valójában a mátrixnak csak az a sora kell, ahol az i-edik diák

jegyei találhatóak. Ezért az alprogram bemenő paramétere a mátrix aktuális

sora lesz, ami egy egydimenziós tömb, és magát a diák sorszámát nem is kell

átadni.

bool Excellent(const vector<int> &v)

{

bool l = true;

Page 295: PROGRAMOZÁSEz 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

295

for(int j=0; l && j<(int)v.size(); ++j){

l = 5 == v[j];

}

return l;

}

Függvények hívási láncolata

Az alábbi kapcsolatrendszerből az olvasható ki, hogy a programban szerepelő

függvények melyik másik függvényeket hívják meg a működésük során.

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)

main()

ReadMarks() ReadInt()

Nat()

Mark()

Excellent()

Page 296: PROGRAMOZÁSEz 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

296

{

int n = ReadInt("Tanulók száma: ",

"Természetes szám!", Nat);

int m = ReadInt("Tárgyak száma: ",

"Természetes szám!", Nat);

reg.resize(n);

for(int i=0; i<n; ++i){

reg[i].resize(m);

cout << i+1 << ". tanuló eredményei\n";

for(int j=0; j<m; ++j){

cout << "\t" << j+1 << ". tantárgy: ";

reg[i][j] = ReadInt("",

"1 és 5 közötti szám!", Mark);

}

}

}

A ReadMarks()egy n×m-es mátrix méretét határozza meg, majd 1 és

5 közötti egész számokkal feltölti. A mátrixot eredmény paraméterváltozó

segítségével adja vissza (hivatkozás szerinti paraméterátadás). A tervvel

ellentétben a mátrix sorai és oszlopai 0-val kezdődően indexeltek.

Először a mátrix méreteinek bekérésére kerül sor, majd (ehhez a

ReadInt() függvényt hívjuk meg a Nat() ellenőrző függvénnyel), majd a

mátrix méretének beállítása után az elemeit olvassuk be (ReadInt() a

Mark() segítségével).

Page 297: PROGRAMOZÁSEz 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

297

Lényegesen barátságosabb a program, ha a diákokra és a tantárgyakra

a nevükkel lehet hivatkozni, nem sorszámmal.

void ReadMarks(vector<vector<int> > &reg)

{

int n = ReadInt("Tanulók száma: ",

"Természetes szám!", Nat);

vector<string> student(n);

cout << "Adja meg a tanulók neveit:" << endl;

for(int i=0; i<n; i++) {

cout << i+1 << ". tanuló neve: ";

cin >> student[i];

}

int m = ReadInt("\nTárgyak száma: ",

"Természetes szám!", Nat);

vector<string> subject(m);

cout << "Adja meg a tantárgyakat:" << endl;

for(int j=0; j<m; j++) {

cout << j+1 << ". tantárgy neve: ";

cin >> subject[j];

}

reg.resize(n);

for(int i=0; i<n; ++i){

Page 298: PROGRAMOZÁSEz 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

298

reg[i].resize(m);

cout << student[i] << " eredményei\n ";

for(int j=0; j<m; j++) {

cout << "\t" << subject[j] << ":";

reg[i][j] = ReadInt("",

"1 és 5 közötti szám!", Mark);

}

}

}

A ReadInt() a korábbi fejezetekből már ismert függvény, amelynek

harmadik paramétere számára két speciális függvényt is készítünk:

természetes számot (Nat), valamint 1 és 5 közé eső egész számot (Mark)

elfogadó függvényeket.

bool Nat(int n) { return n>=0; }

bool Mark(int n){ return n>=1 && n<=5; }

Tesztelés

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.

Page 299: PROGRAMOZÁSEz 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

299

d. Egy diák van egy tárggyal, ami ötös ( azaz van kitűnő diák)

illetve nem ötös (azaz nincs kitűnő diák).

e. Több diák közül csak az első, illetve csak az utolsó kitűnő

(azaz van kitűnő diák).

f. Egy diák több tárggyal, amelyek közül csak az első, illetve

csak az utolsó nem ötös (azaz nincs kitűnő diák).

2. Külső lineáris keresés tesztje:

a. Több diák, több tárgy, és van egy kitűnő, aki az első, vagy az

utolsó, vagy a középső a diákok között.

b. Több diák, több tárgy, és mindenki kitűnő.

c. Több diák, több tárgy, és nincs kitűnő.

3. Belső lineáris keresés tesztje:

a. Egy diák több tárggyal, amelyek közül minden ötös (azaz van

kitűnő diák).

b. Egy diák több tárggyal, amelyek között nem minden ötös

(azaz nincs kitűnő diák).

c. Egy diák több tárggyal, amelyek között csak egy nem ötös

(azaz nincs kitűnő diák).

Ezek az esetek lefedik a main() és az Excellent() fehér doboz

tesztelését is, sőt a ReadMarks() függvényét is.

Az érvénytelen adatok kiszűrését a helyesen paraméterezett

ReadInt() függvény végzi. A ReadInt()tesztelésénél egyrészt ki kell

próbálni, hogy az All() ellenőrző függvényre jól működik-e, majd Nat() és

Mark()mellett is. Fehér doboz tesztelésnél egymás után többször is

érvénytelen adatot kell megadni.

1. Egész számok (-302, -1, 0, 1, 2, 3, 4, 5, 6) beolvasása.

2. Negatív egész számok esete.

3. Nem egész szám esete.

Ennek teszteléséhez ki kell próbálni a helytelen adatok (nem szám,

negatív szám, 5-nél nagyobb vagy 1-nél kisebb osztályzat) beírását.

Page 300: PROGRAMOZÁSEz 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

300

Teljes program

#include <iostream>

#include <vector>

#include <string>

using namespace std;

bool Excellent(const vector<int> &v);

void ReadMarks(vector<vector<int> > &reg);

bool Nat(int n) { return n>=0; }

bool Mark(int n){ return n>=1 && n<=5; }

bool All(int n) { return true; }

int ReadInt(const string &msg, const string &errmsg,

bool check(int) = All);

int main()

{

// Osztályzási napló feltöltése

vector<vector<int> > reg;

ReadMarks(reg);

Page 301: PROGRAMOZÁSEz 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

301

// Lineáris keresés

bool exists = false;

for(int i=0; !exists && i<(int)reg.size(); ++i){

exists = Excellent(reg[i]);

}

// Eredmény kiírása

if(exists) cout << "Van kitűnő diák.\n";

else cout << "Nincs kitűnő diák.\n";

return 0;

}

bool Excellent(const vector<int> &v)

{

bool l = true;

for(int j=0; l && j<(int)v.size(); ++j){

l = 5 == v[j];

}

return l;

}

void ReadMarks(vector<vector<int> > &reg)

{

Page 302: PROGRAMOZÁSEz 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

302

int n = ReadInt("Tanulók száma: ",

"Természetes szám!", Nat);

vector<string> student(n);

cout << "Adja meg a tanulók neveit:" << endl;

for(int i=0; i<n; i++) {

cout << i+1 << ". tanuló neve: ";

cin >> student[i];

}

int m = ReadInt("\nTárgyak száma: ",

"Természetes szám!", Nat);

vector<string> subject(m);

cout << "Adja meg a tantárgyakat:" << endl;

for(int j=0; j<m; j++) {

cout << j+1 << ". tantárgy neve: ";

cin >> subject[j];

}

reg.resize(n);

for(int i=0; i<n; ++i){

reg[i].resize(m);

cout << student[i] << " eredményei\n ";

for(int j=0; j<m; j++) {

cout << "\t" << subject[j] << ":";

reg[i][j] = ReadInt("",

Page 303: PROGRAMOZÁSEz 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

303

"1 és 5 közötti szám!", Mark);

}

}

}

int ReadInt(const string &msg, const string &errmsg,

bool check(int))

{

int n;

bool error = true;

string tmp;

do{

cout << msg;

cin >> n;

if(error = cin.fail() || !check(n)){

cout << errmsg << endl;

cin.clear();

}

getline(cin,tmp);

}while(error);

return n;

}

Page 304: PROGRAMOZÁSEz 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

304

20. Feladat: Azonos színű oldalak

Adott két n oldalú egybevágó szabályos sokszög, amelyek oldalait

véletlenszerűen kiszínezték. Hogyan helyezzük egymásra a két sokszöget úgy,

hogy a lehető legtöbb helyen legyenek azonos színű oldalak egymáson!

Specifikáció

Egy sokszög oldalainak színeit egy n elemű tömbben tároljuk. Így

hallgatólagosan rögzítjük, hogy melyik az első oldal, melyik a második, és így

tovább. A tömböt 0-tól n–1-ig indexeljük, értékei a színek.

Az eredmény egy 0 és n–1 közé eső szám lesz, amely azt mutatja meg,

hogy hányszor kell az egyik sokszöget a másikhoz képest elforgatni az óra

járásával megegyező irányban ahhoz, hogy a lehető legtöbb helyen

kerüljenek azonos színű oldalak egymásra. A forgatás a sokszög oldalainak

újra sorszámozását jelenti. Ha például az x sokszöget i egységgel elforgatjuk,

akkor az így kapott változatnak a j-edik oldala az eredeti változat (i+j) mod n-

edik oldala lesz. Ennek színét kell majd a másik sokszög j-edik oldalának

színével összevetni. Az egyezés(i) azt adja majd meg, hogy az y sokszög hány

oldalának színe egyezik meg az i egységgel elforgatott x sokszög megfelelő

oldalának színével.

A = ( x, y : 10 nSzinek .. , k:ℕ )

Ef = ( x = x’ y = y’ n > 2 )

Uf = ( Ef )(, iegyezéskmaxn

i

1

0

max )

ahol egyezés : [0..n–1]𝕃

egyezés(i) =

1

0

1n

njixjy

j

]mod)[(][

Absztrakt program

Page 305: PROGRAMOZÁSEz 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

305

A megoldás egy maximum kiválasztásba ágyazott számlálás. A maximum

kiválasztás a különböző i-kre kiszámolt egyezés(i) értékek között keresi a

legnagyobbat, a számlálás pedig az egyezés(i) értékét állítja elő.

max,k := egyezés(0),0

i = 1 .. n–1 i:ℕ

e := egyezés(i)

e > max

max, k := e, i SKIP

e:=egyezés(i)

e := 0

j = 0 .. n–1 j:ℕ

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

Page 306: PROGRAMOZÁSEz 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

306

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

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

main()

ReadInt() G2()

ReadPoligon()

MaximalFittness() IdenticalEdges()

Page 307: PROGRAMOZÁSEz 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

307

cout << "Első sokszög oldalainak színei:\n";

ReadPoligon(x);

cout << "Második sokszög oldalainak színei:\n";

ReadPoligon(y);

// Eredmény kiírása

cout << "A második sokszöget "

<< MaximalFittness(x,y)

<< " egységgel kell elforgatni \n" ;

return 0;

}

Absztrakt program kódolása

A maximum kiválasztás (MaximalFittness()) szokásos eredményei közül

csak az indexet (ind) kell visszaadni, az érték (max) közönséges lokális

változó lesz. A tervben globális változóként szereplő két tömbre bemenő

paraméterváltozókkal hivatkozunk. Ez a függvény nem ellenőrzi, hogy a két

tömb azonos hosszú-e, ezt itt feltételezhetjük.

int MaximalFittness(const vector<string> &x,

const vector<string> &y)

{

int ind = 0;

int max = IdenticalEdges(x,y,0);

for(int i=0; i<(int)x.size(); ++i){

Page 308: PROGRAMOZÁSEz 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

308

int c = IdenticalEdges(x,y,i);

if(c > max){

max = c; ind = i;

}

}

return ind;

}

A számlálás (IdenticalEdges()) is megkapja paraméterként a két

tömböt valamint az forgatás mértékét, és visszatérési értékként adja majd

meg az illeszkedő oldalak számát.

int IdenticalEdges(const vector<string> &x,

const vector<string> &y,

int i)

{

int n = (int)x.size();

int c = 0;

for(int j=0; j<n; ++j){

if(x[(i+j)%n] == y[j]) ++c;

}

return c;

}

Sokszög beolvasása

Page 309: PROGRAMOZÁSEz 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

309

A ReadPoligon() függvény sztringként olvassa be egy sokszög oldalainak

színét.

void ReadPoligon(vector<string> &t)

{

for(int i=0; i<(int)t.size(); ++i){

cout << "Az " << i+1 << " oldal színe:";

cin >> t[i];

}

}

Tesztelés

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.

Page 310: PROGRAMOZÁSEz 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

310

Ezek elegendőek a MaximalFittness() és az IdenticalEdges()

fehér doboz tesztelésére is. Önmagában az IdenticalEdges() eltérő

hosszú tömbök esetén kiszámíthatatlanul működik, de ezt nem megfelelő

felhasználásnak tekintjük, ezért nem módosítjuk.

A ReadPoligon() tesztelését a fenti tesztesetek lefedik. Ezt könnyű

látni, ha az általa megoldott részfeladathoz (olvassunk be egy sokszöget)

fekete doboz teszteseteket gyártunk, vagy a kód ismeretében fehér doboz

tesztelést végzünk.

Az érvényes adatok beolvasása a ReadInt() helyes működésén múlik.

A main()tesztelése nem igényel a fentieknél újabb teszteseteket.

Page 311: PROGRAMOZÁSEz 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

311

Teljes program

#include <iostream>

#include <vector>

#include <string>

using namespace std;

int MaximalFittness(const vector<string> &x,

const vector<string> &y);

int IdenticalEdges (const vector<string> &x,

const vector<string> &y, int i);

void ReadPoligon(vector<string> &t);

bool G2(int n){ return n>2; }

bool All(int n) { return true; }

int ReadInt(const string &msg, const string &err,

bool check(int) = All);

int main()

{

//Sokszögek beolvasása

int n = ReadInt("A sokszögek oldalszáma: ",

"2-nál nagyobb egész szám legyen!\n", G2);

vector<string> x(n), y(n);

cout << "Első sokszög oldalainak színei:\n";

Page 312: PROGRAMOZÁSEz 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

312

ReadPoligon(x);

cout << "Második sokszög oldalainak színei:\n";

ReadPoligon(y);

//Eredmény kiírása

cout << "A második sokszöget "

<< MaximalFittness(x,y)

<< " egységgel kell elforgatni.\n";

return 0;

}

int MaximalFittness(const vector<string> &x,

const vector<string> &y)

{

int ind = 0;

int max = IdenticalEdges(x,y,0);

for(int i=0; i<(int)x.size(); ++i){

int c = IdenticalEdges(x,y,i);

if(c > max){

max = c; ind = i;

}

}

return ind;

}

Page 313: PROGRAMOZÁSEz 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

313

int IdenticalEdges(const vector<string> &x,

const vector<string> &y,

int i)

{

int =(int)x.size();

int c = 0;

for(int j=0; j<n; ++j){

if(x[(i+j)%n] == y[j]) ++c;

}

return c;

}

void ReadPoligon(vector<string> &t)

{

for(int i=0; i<(int)t.size(); ++i){

cout << "Az " << i+1 << " oldal színe:";

cin >> t[i];

}

}

int ReadInt(const string &msg,

const string &errmsg, bool check(int))

{

int n;

int error = true;

string tmp;

Page 314: PROGRAMOZÁSEz 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

314

do{

cout << msg;

cin >> n;

if(error = cin.fail() || !check(n)){

cout << errmsg << endl;

cin.clear();

}

getline(cin,tmp);

}while(error);

return n;

}

Page 315: PROGRAMOZÁSEz 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

315

21. Feladat: Mátrix párhozamos átlói

Döntsük el, hogy egy adott négyzetes mátrix mindegyik főátlóval párhuzamos

átlójában az elemek összege nulla-e!

Specifikáció

A feladat pontos megfogalmazásához arra lesz szükség, hogy a főátlóval

párhuzamos átlókat ügyesen megsorszámozzuk. Vegyük észre, hogy egy

főátlóval párhuzamos átlóban (ilyen maga a főátló is) az elemek

oszlopindexének és sorindexének különbsége állandó. A főátlóbeli elemek

(t[i,i]) esetén ez a különbség 0, a főátló feletti átlóban 1 (t[i,i+1]), a legfelső

átlóban (t[1,n]) n–1. Közvetlenül a főátló alatti átlóban -1 (t[i,i–1]), a legalsó

átlóban 1–n (t[n,1]). A cél tehát valamelyik olyan átlót megkeresni, amelyik

sorszáma az 1–n .. n–1 intervallum egy eleme. Az összeg(k) a k-adik sorszámú

átló elemeinek összegét adja majd meg.

A = ( t:ℤn×n, l:𝕃)

Ef = ( t = t’ )

Uf = ( t = t’ l =k[1–n .. n–1]: (összeg(k)=0) )

ahol összeg : [1-n..n-1] ℤ

ö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:ℕ

l k ≤ n–1

l := összeg(k) = 0

k := k + 1

Page 316: PROGRAMOZÁSEz 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

316

s:=összeg(k)

s := 0

i = 1 .. n–k i:ℕ

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

Page 317: PROGRAMOZÁSEz 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

317

return 0;

}

Megjegyezzük, hogy a main függvényen kívül mindegyik függvény

használja a t mátrixot, a ReadMatrix() feltölti egy állomány alapján, a

többi pedig hivatkozik rá. Itt indokolt lenne a mátrixot globális változóként

bevezetni, de mégsem engedünk a csábításnak. A mátrixot a main

függvényben definiáljuk, és az erre történő hivatkozást adjuk át a másik

három függvénynek.

Függvények hívási láncolata

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

main() ReadMatrix()

LinSearch() Summation()

Page 318: PROGRAMOZÁSEz 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

318

l = 0 == Summation(t,k);

}

return l;

}

A Summation() a mátrixra hivatkozó paraméterváltozó mellett

bemenő paraméterként kapja meg a tervben is paraméterként szereplő átló

sorszámot, és az átló összegét visszatérési értékként adja vissza.

int Summation(const vector<vector<int> > &t, int k)

{

int n = (int)t.size();

int s = 0;

for(int i=1; i<=n-abs(k); ++i){

s += t[(abs(k)-k+2*i)/2][(abs(k)+k+2*i)/2];

}

return s;

}

Bemenő adatok beolvasása

Egy mátrix szöveges állományból való feltöltése már többször szerepelt a

korábbi alkalmazásokban. Ezt tartalmazza a ReadMatrix() eljárás,

amelynek egyetlen bemenő és egyben eredmény paraméterváltozója a

mátrixra való hivatkozás lesz.

void ReadMatrix(vector<vector<int> > &t)

Page 319: PROGRAMOZÁSEz 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

319

{

ifstream f;

string fname;

bool hiba;

do{

cout << "A mátrixot tartalmazó állomány:";

cin >> fname;

f.open(fname.c_str());

if(hiba = f.fail()){

cout << "Hibás állománynév!\n";

f.clear();

}

}while(hiba);

int n;

f >> n;

t.resize(n);

for(int i=0; i<n; ++i){

t[i].resize(n);

for(int j=0; j<m; ++j) f >> t[i][j];

}

f.close();

}

Page 320: PROGRAMOZÁSEz 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

320

Először az állomány nevét olvassuk be, majd megpróbáljuk megnyitni

az állományt. Ha nem sikerül, új állomány nevet kér a program. Ez tehát egy

helyben lekezelt hibaeset. Ezután következik a mátrix méretének és

tartalmának beolvasása. Ha nem tehetjük fel, hogy a szöveges állomány

helyesen van kitöltve, akkor különféle hibaesetek fordulhatnak elő:

1. Nem egész számot olvasunk.

2. A mátrix mérete nem lehet negatív.

3. Az első két számot követő számok darabszáma kevesebb, mint

az első két szám szorzata, esetleg már első két szám is

hiányzik.

Amennyiben ezeket a hibaeseteket a beolvasásnál csak észrevenni

akarjuk, de a lekezelésüket (a hibákra történő reagálásokat) a főprogramra

bízzuk, akkor a legegyszerűbb, ha kivételkezelést alkalmaznunk. Definiáljuk

kivételekként a lehetséges hiba eseteket úgy, mint egy felsorolt típus

értékeit.

enum Errors{ Non_Integer,

Negativ_Matrix_Size,

Not_Enough_Number };

Ha valamelyik hibaeset bekövetkezik, akkor egy annak megfelelő

kivételt dobunk, az eldobott kivételeket pedig a main függvényben kapjuk el

és kezeljük le.

vector<vector<int> > t;

try{

ReadMatrix(t);

}catch(Errors ex){

switch(ex){

Page 321: PROGRAMOZÁSEz 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

321

case Non_Integer:

cout << "Nem egész szám!\n"; break;

case Negativ_Matrix_Size:

cout << "Negatív sor/oszlopok szám!\n";

break;

case Not_Enough_Number:

cout << "Hiányzó adatok a mátrixból!\n";

break;

default:;

}

exit(1);

}

Módosítsuk a ReadMatrix() eljárást úgy, hogy ha valamelyik

hibaeset bekövetkezik az olvasás során, akkor dobjon egy Errors típusú

kivételt. Erre a lehetőségre utalhat az eljárás fejében a paraméterlista után

elhelyezett throw (Errors) kifejezés, amely azt jelzi, hogy a függvény

ilyen és csak ilyen típusú kivételt dobhat. Ez azonban igen nagy felelősséget

kíván a programozótól, aki ezzel azt vállalja, hogy minden egyéb kivételt,

amely ennek a kódrésznek a végrehajtásakor keletkezik, lekezel.

void ReadMatrix(vector<vector<int> > &t)

{

ifstream f;

bool error;

string str;

Page 322: PROGRAMOZÁSEz 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

322

do{

cout << "Fajl neve:";

cin >> str;

f.open( str.c_str() );

if ( error = f.fail() ){

cout << "Nincs ilyen nevu fajl" << endl;

f.clear();

}

}while (error);

int n = ReadIntFromFile(f);

if(n<0) throw Negativ_Matrix_Size;

t.resize(n);

for(int i=0; i<n; ++i){

t[i].resize(n);

for(int j=0; j<n; ++j){

t[i][j] = ReadIntFromFile(f);

}

}

f.close();

}

A ReadMatrix() eljárás meghív egy segédfüggvényt is, amelyik egy

egész számot próbál beolvasni, és olvasási hiba esetén ez is kivételeket dob.

Ezeket a kivételeket a ReadMatrix() nem kapja el, nem kezeli le, hanem

Page 323: PROGRAMOZÁSEz 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

323

automatikusan tovább dobja, így ezek végül a main függvény

kivételkezelésében csapódnak le.

int ReadIntFromFile(ifstream &f)

{

string str;

f >> str;

if( f.eof() ) throw Not_Enough_Number;

int n = atoi(str.c_str());

if( 0 == n && str != "0" ) throw Non_Integer;

return n;

}

Tesztelés

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.

Page 324: PROGRAMOZÁSEz 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

324

3. Összegzés tesztje:

a. 3×3-s mátrix, a főátlón kívül minden elem pozitív, és a

főátlóbeli nem nulla elemek összege nulla. Válasz: van.

b. 3×3-s mátrix, a főátló feletti átlón kívül minden elem pozitív,

és a főátló feletti átló nem nulla elemeinek összege nulla.

Válasz: van.

c. 3×3-s mátrix, a főátló alatti átlón kívül minden elem pozitív,

és a főátló alatti átló nem nulla elemeinek összege nulla.

Válasz: van.

Ezek elegendőek a LinSearch() és az Summation() fehér doboz

tesztelésére is.

A ReadMatrix() tesztelését a fenti tesztesetek lefedik. Az érvénytelen

adatok kivédését a ReadIntFromFile() függvény végzi.

Tesztadatokat kell viszont generálni a kivételkezelés mindhárom esetére,

ami lényegében a main()tesztelését jelenti.

1. Rossz formátumú számok a szöveges fájlban.

2. Negatív a mátrix mérete a szöveges fájlban.

3. Nincs kellő darabszámú elem a szöveges fájlban.

Page 325: PROGRAMOZÁSEz 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

325

Teljes program

#include <iostream>

#include <fstream>

#include <vector>

#include <string>

#include <cmath>

#include <cstdlib>

using namespace std;

enum Errors{ Non_Integer,

Negativ_Matrix_Size,

Not_Enough_Number };

bool LinSearch(const vector<vector<int> > &t);

int Summation(const vector<vector<int> > &t, int k);

void ReadMatrix(vector<vector<int> > &t);

int ReadIntFromFile(ifstream &f);

int main()

{

// Mátrix létrehozása és kitöltése

vector<vector<int> > t;

try{

ReadMatrix(t);

Page 326: PROGRAMOZÁSEz 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

326

}catch(Errors ex){

switch(ex){

case Non_Integer:

cout << "Nem egész szám!\n"; break;

case Negativ_Matrix_Size:

cout << "Negatív sor/oszlopok szám!\n";

break;

case Not_Enough_Number:

cout << "Hiányzó adatok a mátrixból!\n";

break;

default:;

}

exit(1);

}

// Eredmény kiírása

int ind;

if(LinSearch(t,ind))

cout << "Minden átló elemösszege nulla.\n";

else

cout << "Van nem zéróösszegű átló is.\n";

return 0;

}

bool LinSearch(const vector<vector<int> > &t)

Page 327: PROGRAMOZÁSEz 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

327

{

int n = (int)t.size();

bool l = false;

for(int k=1-n; !l && k<=n-1; ++k){

l = 0 == Summation(t,k);

}

return l;

}

int Summation(const vector<vector<int> > &t, int k)

{

int n = (int)t.size();

int s = 0;

for(int i=1; i<=n-abs(k); ++i){

s += t[(abs(k)-k+2*i)/2][(abs(k)+k+2*i)/2];

}

return s;

}

void ReadMatrix(vector<vector<int> > &t)

{

ifstream f;

bool error;

string str;

do{

Page 328: PROGRAMOZÁSEz 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

328

cout << "Fajl neve:";

cin >> str;

f.open( str.c_str() );

if ( error = f.fail() ){

cout << "Nincs ilyen nevu fajl" << endl;

f.clear();

}

}while (error);

int n = ReadIntFromFile(f);

if(n<0) throw Negativ_Matrix_Size;

t.resize(n);

for(int i=0; i<n; ++i){

t[i].resize(n);

for(int j=0; j<n; ++j){

t[i][j] = ReadIntFromFile(f);

}

}

f.close();

}

int ReadIntFromFile(ifstream &f)

{

Page 329: PROGRAMOZÁSEz 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

329

string str;

f >> str;

if( f.eof() ) throw Not_Enough_Number;

int n = atoi(str.c_str());

if( 0 == n && str != "0" ) throw Non_Integer;

return n;

}

Page 330: PROGRAMOZÁSEz 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

330

C++ kislexikon

kivétel tetszőleges érték vagy objektum

kivétel dobása throw kivétel

kivételt dobó

függvény

int fv(…) throw (kivétel típusa)

kivétel figyelése try{

}

kivétel elkapása és

kezelése

catch(típus változó){

if(kivétel==változó) …

}

Page 331: PROGRAMOZÁSEz 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

331

9. Fordítási egységekre bontott program

Egy összetett program kódja jóval áttekinthetőbbé válik, ha a logikailag

összetartozó részeket (alprogramokat, azok által közösen használt

változókat, konstansokat, típusdefiníciókat) külön egységként, úgynevezett

modulokban írjuk le. Tulajdonképpen egy kommentekkel és üres sorokkal

elhatárolt kódrészt is lehet ilyen külön egységnek tekinteni, de egy

alprogramot már minden ellenérzés nélkül önálló modulként emlegethetünk.

Modul az is, ha több, valamilyen szempont szerint összetartozó alprogramot

gyűjtünk össze, sőt egy ilyen modul kiegészülhet az alprogramjai által

közösen használt típusok, konstansok és változók definícióival. Ha ezeknek a

valamilyen szempont szerint összegyűjtött alprogramoknak, típusoknak,

változóknak és konstansoknak a gyűjteményét önálló fordítási egységben

írjuk le, akkor a modult csomagnak nevezzük.

Egy program csomagokra bontása számos előnnyel jár. Egy csomag a

program többi része nélkül fordítható, sőt megfelelő keret programmal

önállóan tesztelhető. Ez lehetőséget teremt arra, hogy egy program egyes

csomagjait más-más programozó egymással párhuzamosan fejleszthesse és

így a program csoportmunkában készüljön, valamint hogy ugyanazt a

csomagot több különböző programban is felhasználhassunk.

A program egyes részéinek egymástól való fizikai elkülönítése erősíti az

egyes részeken belüli összetartozást, még átláthatóbbá teszi az egyes részek

közötti kapcsolatokat. Egy csomag felhasználása azt jelenti, hogy a benne

definiált elemeket közvetve vagy közvetlenül igénybe vesszük abban a

programban, amelynek ez a csomag a része. Ehhez természetesen valahogy

jelölni kell valahogyan azt, ha a program egy szakaszában használni kívánjuk

egy csomag elemeit.

Egy csomag leírása magába foglalja a benne definiált típusoknak,

változóknak, konstansoknak és alprogramoknak a leírását, azaz a csomag

törzsét. Meg kell adni azt is, hogy melyek azok az elemek, amelyeket a

csomagon kívül is közvetlenül használhatunk (mit szolgáltat, exportál a

csomag), melyek azok, amelyek csak „belső” (csomagon belüli) használatra

Page 332: PROGRAMOZÁSEz 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

332

készültek. Magában a csomag törzsében lehetnek olyan részek, amelyek más

csomagok szolgáltatásait igénylik, importálják.

Implementációs stratégia

A kód modulokra bontása, majd bizonyos moduljainak önálló fordítási

egységbe, azaz csomagba zárása kellő tapasztalatot igényel. Csak sok

program megírása után alakul ki egy programozóban az a készség, hogy

milyen logika mentén érdemes egy csomagot kialakítani.

Az egyik rendező elv az azonos célú, hasonló funkciójú alprogramok

egybegyűjtése. Például egy csomagba tehetünk különféle adatbeolvasó

eljárásokat, amelyek közül az egyik egy egész számot, a másik egy valós

számot, a harmadik egy tömböt, stb. olvashat be. Külön csomagba

kerülhetnek ezen eljárásoknak a billentyűzetről olvasó, külön csomagba a

szöveges állományból olvasó változatai. Egy másik példa az, amikor bonyolult

matematikai számításokat végző eljárásokat (mondjuk lineáris

egyenletrendszerek különféle megoldásait vagy integrálszámító numerikus

módszereket) gyűjtünk egy csomagba.

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

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.

Page 333: PROGRAMOZÁSEz 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

333

(ezt a 11. fejezetben mutatjuk be), de egy kezdetleges próbálkozást már e

fejezet 23. feladatának megoldásában is mutatunk.

A csomag az alprogramjain kívül tartalmazhat azok által közösen

használt típus-, változó- és konstans, továbbá úgynevezett belső modulokat

is. Éppen ezért nagyon alaposan meg kell azt gondolni, hogy egy csomag

mely elemeit lehessen a csomagon kívül használni, és amelyek legyenek

azok, amelyek csak belső használatra szolgálnak. Különösen kényes kérdés a

csomagbeli változók külső használatának engedélyezése. Ezek ugyanis olyan

globális változók, amelyek a csomagban is láthatóak és a program azon

részén is, ahol a csomagot használjuk, de nem láthatók más csomagokban. A

globális változók használatával szemben már eddig is megfogalmaztuk a

fenntartásainkat, és ezen most sem kívánunk változtatni.

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,

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.

Page 334: PROGRAMOZÁSEz 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

334

típusok, konstansok és alprogramok. A komponens implementációja (törzse)

e szolgáltatások biztosításához szükséges exportált és nem exportált elemek

kódját tartalmazza. Egy komponens maga is támaszkodhat más

komponensek szolgáltatásaira, használhat máshol definiált változókat,

konstansokat, típusokat, alprogramokat, azaz rendelkezik igényeinek,

szükségleteinek gyűjteményével (import listával).

Nyelvi elemek

A több csomagra bontott C++ kód a main függvényt tartalmazó fő-

forrásállományon (alapértelmezésben main.cpp) kívül tetszőleges számú

forrásállományból állhat, amelyek egy-egy csomag törzsét tartalmazzák.

Minden ilyen forrásállományhoz létre szoktak hozni egy úgynevezett

fejállományt is, amely az adott csomag interfészének (exportlistájának)

megadására szolgál. Így egy csomag tulajdonképpen két állományban, egy

fej- és egy forrásállományban helyezkedik el.

A fejállományba a csomag azon elemei (típus definíciók, alprogram

deklarációk, konstans definíciók, változók, és olyan összetettebb belső

modulok definíciói is, mint az osztályok) kerülnek, amelyeket exportálni

szeretnénk. A csomag minden egyéb része (alprogramok és egyéb típusok

definíciója, a csomagra nézve globális változók) a forrásállományba kerül.

Bár nem kötelező, de célszerű az összetartozó fej- és forrásállomány

nevét ugyanannak választani, és csak az állománynév kiterjesztésével

megkülönböztetni. A forrásállomány egy önmagában is lefordítható C++

kódot tartalmaz, ezért a kiterjesztése „cpp”. A fejállomány viszont nem

fordítható önállóan, ezt azzal jelezzük, hogy a kiterjesztése „cpp” helyett „h”.

A fejállományt mindig bemásoljuk (#include) a hozzá tartozó

forrásállományba, hiszen a csomag testének is ismernie (látnia) kell önmaga

interfészét. De bemásoljuk a csomag fejállományát a program minden olyan

állományába is, ahol a C++ kód hivatkozik a fejállományban felkínált

valamelyik szolgáltatásra, azaz importálni kívánja azokat.

Egy forrásállományba egy fejállomány nemcsak közvetlen másolással

kerülhet be, hanem közvetve is egy olyan másik fejállomány bemásolásakor,

amely az első fejállományt bemásolja. Elkerülendő, hogy ezáltal ugyanaz a

Page 335: PROGRAMOZÁSEz 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

335

fejállomány többszörösen bemásolódjon egy kódba, mert a fordító program

ilyenkor hibát jelez. Ezért a bemásolásokat végző előfordítót úgynevezett

állomány-őrszemekkel szokták befolyásolni. A fejállományok első sorába az

#ifndef NEV utasítást (a NEV -nek ajánlott a fejállomány fizikai nevét

választani), a második sorba a #define NEV utasítást, és az utolsó sorba a

#endif utasítást írjuk.

A forrásállományok (és ez vonatkozik a fő-forrásállományra is)

rendszerint függvénydefiníciókat tartalmaznak, de megjelenhetnek benne

csak az adott forrásállományra érvényes típusdefiníciók, konstansok, és

ritkán (globális) változók is. Ilyenkor a forrásállománynak az elején helyezzük

el a típus-, konstans- és változó-definíciókat, majd a kizárólag ebben az

állományban használt függvények deklarációit, végül ezt követik a

függvények definíciói, de nem csak a belső függvényeké, hanem a

forrásállományhoz esetleg hozzátartozó fejállományban deklarált

függvényeké is. A fő-forrásállományban elsőként a main függvényt szokás

definiálni. Ezt a konvenciót már eddig is használtuk az egyetlen

forrásállományból álló programjainkban.

A csomagok önálló fordítási egységek, azaz egymástól függetlenül

lehet őket fordítani. Egy programmá a szerkesztés során forrnak össze.

Továbbra is igaz az, hogy a C++ programnak összességében egy main

függvénnyel kell rendelkezni ahhoz, hogy futtatható legyen. Integrált

fejlesztő eszközök használatakor a csomagokat közös projektbe szokás

összerakni, innen fogja tudni a futtató környezet, hogy mely lefordított

csomagokat kell egy programmá összeszerkeszteni és futtatni.

Ha egy csomag forrásállományában olyan globális változót szeretnénk

használni, amelyet kizárólag belső elemként, csak a csomag alprogramjai

számára akarunk elérhetővé tenni, akkor ezt a szándékunkat a static

kulcsszó feltüntetésével jelezhetjük. Ennek használata nélkül más csomagok

számára is láthatóvá tehető egy csomag globális változója, amennyiben

annak deklarációját a másik csomagban az extern kulcsszó megadása után

megismételjük. Ha egy mód van rá, ne használjuk ezt a lehetőséget. (Ha

mégis szükség lenne olyan változókra, amelyeket több csomagban is

használni szeretnénk, akkor deklaráljuk azokat egy külön fejállományban,

amelyet az összes érintett csomag forrásállományában másoljunk be.)

Page 336: PROGRAMOZÁSEz 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

336

Csomagokat eddigi programjainkban is használtunk már, de azokat

nem mi készítettük el, hanem készen álltak a rendelkezésünkre. Gondoljunk

az iostream, fstream, string, vector, math.h csomagokra, amelyek

szolgáltatásait (típusait, függvényeit) igénybe vettük, azok a programunk

részei lettek, de a fejlesztésükkel nem kellett foglalkoznunk.

A névterek használatával rugalmasan lehet a programunk

azonosítóinak hatáskörét (láthatóságát) befolyásolni. Használatuk lehetővé

teszi, hogy azonos nevű, de eltérő jelentésű azonosítókat definiáljunk,

amelyeket az őket tartalmazó névtér nevének segítségével minősítve

különböztethetünk meg. Ehhez elég az azonosító neve elé írni a definíciót

tartalmazó névtér nevét és a :: szimbólumot. A minősítés elhagyható a kód

azon részén, amely előtt a using namespace <névtérnév> utasítás

található.

A standard névtér elemeit használó forrásállományok elején mindig

célszerű a using namespace std utasítást elhelyezni. A fejállományokba

ellenben nem szokás ezt beírni, ezért ha hivatkozunk a standard névtér

elemeire, akkor azokat csak az std:: minősítéssel tudjuk használni.

Page 337: PROGRAMOZÁSEz 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

337

22. Feladat: Műkorcsolya verseny

Soroljuk fel azokat a műkorcsolyázó versenyzőket, akik holtversenyben az

első helyen végeztek a kötelező gyakorlatuk bemutatása után. Az n versenyző

programját m tagú zsűri pontozta, amelyből egy versenyző összesített

pontszámát úgy kapjuk, hogy a legjobb és legrosszabb pontot elvéve a többi

pontszám átlagát képezzük. (A feladat megtalálható az első kötet 6.9.

példájaként.)

Specifikáció

A = ( t : ℝn×m, s : ℕ* )

Ef = ( t=t’ n≥1 m>2 )

Uf = ( Ef s =

i

ipont

n

1imax)(

max = )(ipontn

1imax )

ahol pont:[1..n]ℝ

pont(i) = (

m

1i

jit ],[ – ],[ jitm

1jmax – ],[ jit

m

1jmin )/(m–2)

Absztrakt program

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.

max, p inicializálás p:ℝn, max:ℝ

s := <>

i = 1..n i:ℕ

max=p[i]

s := s <i> SKIP

Page 338: PROGRAMOZÁSEz 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

338

Ezt megelőzi a versenyzők összesített pontszámait egy tömbben

elhelyező és közben ezek közül a legnagyobb összesített pontszámot is

meghatározó előkészítő rész (max, p inicializálása), amely egy összegzés

(valójában egy összefűzés) és egy maximum kiválasztás ciklusainak

összevonásaként állt elő.

max, p inicializálás d := pont(i)

p[1] := pont(1) o,maxi,mini := t[i,1], t[i,1],

t[i,1]

maxi,

mini, o:ℝ

max := p[1] j = 2..m

i = 2..n i:ℕ o := o+t[i,j] j:ℕ

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

Page 339: PROGRAMOZÁSEz 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

339

valamilyen értelemben rokon funkciójú (itt mátrix beolvasását és kiírását

végző) függvényeket gyűjtjük egy csomagba.

A tervben szereplő tömbök (t, p) a kódban 0-tól indexelt tömbök

lesznek, ennek megfelelően a tömböket feldolgozó számlálós ciklusok

indextartománya is eggyel eltolódik az absztrakt programokhoz képest.

A program kerete

A vezérlést, azaz az alprogramok megfelelő sorrendben történő meghívását a

main függvény biztosítja.

int main()

{

vector<vector<double> > t;

Fill(t);

if(t.size()<1 or t[i].size()<3){

cout << "Nem jó méretű a mátrix\n";

return 1;

}

cout << "Pontszamok: \n";

Write(t);

vector<double> p(t.size());

double max;

Init(t,p,max);

Select(p,max);

Page 340: PROGRAMOZÁSEz 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

340

return 0;

}

Függvények hívási láncolata

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

main()

Fill()

ReadIntFromFile()

ReadRealFromFile()

Write()

Init() Result()

Select()

Page 341: PROGRAMOZÁSEz 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

341

9-4. ábra. Komponens szerkezet

Absztrakt program kódolása

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

}

main.cpp

main()

Init()

Select()

Result()

matrix.h - matrix.cpp

Fill()

Write()

ReadIntFromFile

ReadRealFromFile

Page 342: PROGRAMOZÁSEz 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

342

}

A Result() bemenő adata a mátrix egy sora, visszatérési értéke egy

valós szám. A tervben szereplő két elágazás szekvenciája átalakítható egy

háromágú elágazássá (a harmadik ág üres) hiszen a v[j]>max és a v[j]<min

feltételek egymást kizárják.

double Result(const vector<double> &v)

{

double o, maxi, mini;

o = maxi = mini = v[0];

for(int j=1; j<(int)v.size(); ++j){

o = o + v[j];

if(v[j]>maxi) maxi = v[j];

else if(v[j]<mini) mini = v[j];

}

return (o-maxi-mini)/(v.size()-2);

}

A Select()a p valós számokat tartalmazó tömb és a max valós

számot kapja bemenetként.

void Select(const vector<double> &p, double max)

{

cout << "A legjobb versenyzők:\n";

for(int i=0; i<(int)p.size(); ++i){

Page 343: PROGRAMOZÁSEz 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

343

if(p[i] == max) cout << i+1 << "\t";

}

}

Mátrix csomag

Ez a csomag egy mátrix szöveges állományból történő feltöltését és

kiírását biztosítja. A csomag szolgáltatásait (a Fill() és a Write()

alprogramok deklarációit) a matrix.h fejállomány sorolja fel. Ez tehát az

export lista. Mivel itt a vector<> típust használjuk, ezért hivatkoznunk kell a

könyvtári vector csomagra is. Ez a csomagunk import listája. A

fejállományokban nem szokás a using namespace std utasítást

használni, ezért, mivel a vector az std (standard névtér) eleme, a vector

azonosítója elé ki kell írni az std:: minősítést. A fejállományt megfelelő

őrszem-utasítással kell ellátni.

#ifndef MATRIX_H

#define MATRIX_H

#include <vector>

void Fill( std::vector<std::vector<double> > &t);

void Write(

const std::vector<std::vector<double> > &t);

#endif

Ezt a fejállományt „inklúdolja” majd mind a main.cpp, ahonnan az

alprogramokat meghívjuk, mind a matrix.cpp, ahol az alprogramok

Page 344: PROGRAMOZÁSEz 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

344

definíciója található. A Fill() eredmény paramétere és a Write()

bemenő paramétere egyaránt a valós számokat tartalmazó mátrix.

A komponensünk törzsét, a Fill() és a Write() alprogramok kódját

a matrix.cpp tartalmazza.

A mátrix beolvasásánál (Fill()) feltesszük, hogy az adatokat

tartalmazó szöveges állományban először két természetes számot (sorok és

oszlopok száma) találunk, majd azokat követően annyi darab valós számot

(mátrix elemei), amennyi a két természetes szám szorzata. A számokat

legalább egy elválasztó jel (szóköz, tabulátor jel vagy sorvége jel) határolja. Ez

a forma lehetővé teszi például azt, hogy az állomány első sorában a sorok és

oszlopok számát adjuk meg, majd soronként a mátrix egyes soraihoz tartozó

elemeket.

void Fill(vector<vector<double> > &t)

{

ifstream f;

bool hiba;

string str;

do{

cout << "Fájl neve:";

cin >> str;

f.open( str.c_str() );

if ( hiba = f.fail() ){

cout << "Nincs ilyen nevű fájl" << endl;

f.clear();

}

}while (hiba);

Page 345: PROGRAMOZÁSEz 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

345

int n, m;

f >> n >> m;

t.resize(n);

for(int i=0; i<n; ++i){

t[i].resize(m);

for(int j=0; j<m; ++j) f >> t[i][j];

}

f.close();

}

Módosítsuk a mátrixnak szöveges állományból történő feltöltését úgy,

hogy az olvasásnál bekövetkező esetleges hibákat is figyeljük, és erről a

feltöltést meghívó programot kivétel dobásával értesítjük.

Az előforduló hibaesetek:

1. Nem egész számot olvasunk.

2. Nem valós számot olvasunk.

3. A mátrix mérete nem lehet negatív.

4. Az első két számot követő számok darabszáma kevesebb, mint

az első két szám szorzata, esetleg már első két szám sincs.

Nem ide tartozik az a hiba, amikor nincs legalább egy sora és három

oszlopa a mátrixnak. Ezt ugyanis a konkrét feladat előfeltétele írja elő, tehát

a főprogramban és nem egy általános mátrix-beolvasó eljárásban kell

vizsgálni. Ugyancsak nem ide soroljuk a nem létező állománynév hibaesetet,

mert ezt helyben, az olvasáson belül kezeljük.

A kivételek típusát a matrix.h-ban definiáljuk. Ez bármi lehet:

legegyszerűbb esetben a hibaeset megnevezése (ami lehet egy sztring vagy

egy felsorolt típus eleme), összetettebb esetben egy több adattagot is

Page 346: PROGRAMOZÁSEz 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

346

hordozó objektum. Mi itt szeretnénk kivételként visszaadni a hibaeset

megnevezését és bizonyos esetekben magát a beolvasott hibás adatot is.

Ezért a kivétel egy rekord (struct) lesz, amelynek egyik tagja a hibaeset

neve, amit egy felsorolt típus értékei közül választhatunk, másik tagja pedig

egy üzenet (string). Négy hibaesetet különböztetünk meg a beolvasás

során előforduló négy féle hiba miatt.

enum Errors{Non_Integer, Non_Real,

Negativ_Matrix_Size, Not_Enough_Number};

struct Exceptions{

Errors code;

std::string msg;

};

Egészítsük ki a Fill() eljárást úgy, hogy ha valamelyik hibaeset

bekövetkezik az olvasás során, akkor dobjon egy kivételt. Ez arra kényszeríti

a komponens használóját (esetünkben a main függvényt), hogy készüljön fel

mindenféle Exceptions típusú kivételt lekezelésére.

Az alprogram eleje nem változik:

void Fill(vector<vector<double> > &t)

{

ifstream f;

bool hiba;

string str;

do{

cout << "Fájl neve:";

Page 347: PROGRAMOZÁSEz 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

347

cin >> str;

f.open( str.c_str() );

if ( hiba = f.fail() ){

cout << "Nincs ilyen nevű fájl" << endl;

f.clear();

}

}while (hiba);

Egy kivételnek ki kell tölteni a hibakód- és az üzenet mezőjét. Az

üzenetbe célszerű beleírni azt az adatot is, amely a kivétel dobását kiváltotta.

Esetünkben az adatok számok, amelyeket sztringgé kell konvertálnunk. Az

alábbi kódban ezt egy ostringstream típusú objektum segítségével

végezzük el. Ehhez szükségünk van az sstream csomagra.

int n = ReadIntFromFile(f);

if(n<0) {

Exceptions ex;

ex.code = Negativ_Matrix_Size;

ostringstream ss;

ss << n;

ex.msg = ss.str();

throw ex;

}

int m = ReadIntFromFile(f);

if(m<0){

Page 348: PROGRAMOZÁSEz 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

348

Exceptions ex;

ex.code = Negativ_Matrix_Size;

ostringstream ss;

ss << m;

ex.msg = ss.str();

throw ex;

}

t.resize(n);

for(int i=0; i<n; ++i){

t[i].resize(m);

for(int j=0; j<m; ++j){

t[i][j] = ReadRealFromFile(f);

}

}

f.close();

}

A Fill() eljárás meghív két segédfüggvényt is:

ReadIntFromFile() és ReadRealFromFile(). Egyik egy egész, másik

egy valós számot próbál beolvasni. Ezekre a segédfüggvényekre a

főprogramban nincs közvetlenül szükség, a matrix komponens nem is

ajánlja fel őket külön szolgáltatásként, hiszen nem szerepelnek az export

listában, azaz a matrix.h-ban. Ezek a függvények is dobnak Exceptions

típusú kivételt, amelyet a Fill() nem kezel le, hanem tovább dob.

int ReadIntFromFile(ifstream &f)

Page 349: PROGRAMOZÁSEz 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

349

{

string str;

f >> str;

if( f.eof() ){

Exceptions ex;

ex.code = Not_Enough_Number;

throw ex;

}

int n = atoi(str.c_str());

if( 0 == n && str != "0" ){

Exceptions ex;

ex.code = Non_Integer;

ex.msg = str;

throw ex;

}

return n;

}

Olvasási hiba esetén mindkét segédfüggvény kivételeket dobhat,

amelyeket azonban a Fill() nem kezel le, hanem automatikusan

továbbdobja az őt hívó programrésznek. Ezek éppen olyan Exception

típusú kivételek, mint amelyeket a Fill() amúgy is dobni tud.

double ReadRealFromFile(ifstream &f)

{

Page 350: PROGRAMOZÁSEz 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

350

string str;

f >> str;

if( f.eof() ){

Exceptions ex;

ex.code = Not_Enough_Number;

throw ex;

}

double a = atof(str.c_str());

if( 0 == a && str != "0" ){

Exceptions ex;

ex.code = Non_Real;

ex.msg = str;

throw ex;

}

return a;

}

A main függvényben kell elkapnunk és lekezelnünk a Fill() által

dobható kivételeket. Ott ahol a kivétel adatat is lényeges, nemcsak a

hibaeset nevát, de az üzenetét is kiírjuk. Láthatjuk, hogy külön ellenőrizzük,

hogy a mátrix legalább 1×3-as legyen.

vector<vector<double> > t;

try{

Fill(t);

Page 351: PROGRAMOZÁSEz 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

351

}catch(Exceptions ex){

switch(ex.code){

case Non_Integer:

cout << "Rossz formájú egész szám!"

<< ex.msg << endl; break;

case Non_Real:

cout << "Rossz formájú valós szám!"

<< ex.msg << endl; break;

case Negativ_Matrix_Size:

cout << "Sor, oszlop szám nem negatív!"

<< ex.msg << endl; break;

case Not_Enough_Number:

cout << "Hiányzó adatok!\n"; break;

default:;

}

exit(1);

}

if(t.size()<1){

cout<<"Legalább 1 versenyző kell!"; exit(1);

}

if(t[0].size()<3){

cout<<"Legalább 3 zsűritag kell"; exit(1);

}

Page 352: PROGRAMOZÁSEz 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

352

A csomag másik eleme, a Write(), egy valós számokat tartalmazó

mátrixot ír ki.

void Write(const vector<vector<double> > &t)

{

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;

}

}

Tesztelés

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

Page 353: PROGRAMOZÁSEz 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

353

lásd még fenti esetek közül: b és c-t.

3. Az Result()-beli összegzés tesztje:

a. 1 versenyző és 3 zsűritag. Válasz: <1>.

b. 1 versenyző és 5 zsűritag. Válasz: <1>.

4. Az Result()-beli maximum kiválasztás tesztje:

5. Az Result()-beli maximum kiválasztás tesztje:

a. 1 versenyző és 3 zsűritag, és az első adta a legtöbb pontot.

Válasz: <1>.

b. 1 versenyző és 3 zsűritag, és az utolsó adta a legtöbb pontot.

Válasz: <1>.

c. 1 versenyző és 3 zsűritag, és az első kettő adta a legtöbb

pontot. Válasz: <1>.

d. 1 versenyző és 3 zsűritag, és az utolsó kettő adta a legtöbb

pontot. Válasz: <1>.

e. 1 versenyző és 3 zsűritag, és mind ugyanannyi pontot adott.

Válasz: <1>.

6. Az Result()-beli minimum kiválasztás tesztje a 6. pont alapján.

Modulonkénti fehér doboz tesztelés:

A fenti esetek elegendőek az Init(), a Result() és a Select()

tesztelésére.

Fill() tesztelése:

1. Nem létező bemenő állomány esete.

2. Egész számok helyett más adatok a szöveges állományban.

3. Valós számok helyett más adatok (betű, sztring) a szöveges

állományban.

4. A szöveges állományban megadott darabszámnál kevesebb adat van

a fájlban.

Write() tesztelése: a korábbi tesztesetek lefedik ennek a modulnak a

tesztelését.

Page 354: PROGRAMOZÁSEz 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

354

Page 355: PROGRAMOZÁSEz 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

355

Teljes program

main.cpp:

#include <iostream>

#include <vector>

#include <cstdlib>

#include "matrix.h"

using namespace std;

void Init(const vector<vector<double> > &t,

vector<double> &p,double &max);

void Select(const vector<double> &p, double max);

double Result(const vector<double> &v);

int main()

{

vector<vector<double> > t;

try{

Fill(t);

}catch(Exceptions ex){

switch(ex.code){

case Non_Integer:

cout << "Rossz formájú egész szám!"

Page 356: PROGRAMOZÁSEz 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

356

<< ex.msg << endl; break;

case Non_Real:

cout << "Rossz formájú valós szám!"

<< ex.msg << endl; break;

case Negativ_Matrix_Size:

cout << "Sor, oszlop szám nem negatív!"

<< ex.msg << endl; break;

case Not_Enough_Number:

cout << "Hiányzó adatok!\n"; break;

default:;

}

exit(1);

}

if(t.size()<1){

cout<<"Legalább 1 versenyző kell!"; exit(1);

}

if(t[0].size()<3){

cout<<"Legalább 3 zsűritag kell"; exit(1);

}

cout << "Pontszamok: \n";

Page 357: PROGRAMOZÁSEz 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

357

Write(t);

vector<double> p(t.size());

double max;

Init(t,p,max);

Select(p,max);

return 0;

}

void Init(const vector<vector<double> > &t,

vector<double> &p, double &max)

{

p[0] = Result(t[0]);

max = p[0];

for(int i=1; i<(int)t.size(); ++i){

p[i] = Result(t[i]);

if(p[i]>max) max = p[i];

}

}

void Select(const vector<double> &p, double max)

{

cout << "A legjobb versenyzők:\n";

for(int i=0; i<(int)p.size(); ++i){

if(p[i] == max) cout << i+1 << "\t";

Page 358: PROGRAMOZÁSEz 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

358

}

}

double Result(const vector<double> &v)

{

double o, maxi, mini;

o = maxi = mini = v[0];

for(int j=1; j<(int)v.size(); ++j){

o = o + v[j];

if(v[j]>maxi) maxi = v[j];

else if(v[j]<mini) mini = v[j];

}

return (o-maxi-mini)/(v.size()-2);

}

Page 359: PROGRAMOZÁSEz 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

359

matrix.h:

#ifndef MATRIX_H

#define MATRIX_H

#include <vector>

#include <string>

enum Errors{Non_Integer, Non_Real,

Negativ_Matrix_Size, Not_Enough_Number};

struct Exceptions{

Errors code;

std::string msg;

};

void Fill( std::vector<std::vector<double> > &t);

void Write(

const std::vector<std::vector<double> > &t);

#endif

matrix.cpp:

#include "matrix.h"

#include <iostream>

#include <cstdlib>

#include <fstream>

Page 360: PROGRAMOZÁSEz 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

360

#include <sstream>

using namespace std;

int ReadIntFromFile(ifstream &f);

double ReadRealFromFile(ifstream &f);

void Write(const vector<vector<double> > &t)

{

for(int i=0; i<(int)t.size(); ++i){

for(int j=0; j<(int)t[i].size(); ++j){

cout << t[i][j] <<"\t";

}

cout << endl;

}

}

void Fill(vector<vector<double> > &t)

{

ifstream f;

bool hiba;

Page 361: PROGRAMOZÁSEz 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

361

string str;

do{

cout << "Fájl neve:";

cin >> str;

f.open( str.c_str() );

if ( hiba = f.fail() ){

cout << "Nincs ilyen nevű fájl" << endl;

f.clear();

}

}while (hiba);

int n = ReadIntFromFile(f);

if(n<0) {

Exceptions ex;

ex.code = Negativ_Matrix_Size;

ostringstream ss;

ss << n;

ex.msg = ss.str();

throw ex;

}

int m = ReadIntFromFile(f);

if(m<0){

Exceptions ex;

Page 362: PROGRAMOZÁSEz 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

362

ex.code = Negativ_Matrix_Size;

ostringstream ss;

ss << m;

ex.msg = ss.str();

throw ex;

}

t.resize(n);

for(int i=0; i<n; ++i){

t[i].resize(m);

for(int j=0; j<m; ++j){

t[i][j] = ReadRealFromFile(f);

}

}

f.close();

}

int ReadIntFromFile(ifstream &f)

{

string str;

f >> str;

if( f.eof() ){

Page 363: PROGRAMOZÁSEz 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

363

Exceptions ex;

ex.code = Not_Enough_Number;

throw ex;

}

int n = atoi(str.c_str());

if( 0 == n && str != "0" ){

Exceptions ex;

ex.code = Non_Integer;

ex.msg = str;

throw ex;

}

return n;

}

double ReadRealFromFile(ifstream &f) {

string str;

f >> str;

if( f.eof() ){

Exceptions ex;

ex.code = Not_Enough_Number;

throw ex;

}

Page 364: PROGRAMOZÁSEz 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

364

double a = atof(str.c_str());

if( 0 == a && str != "0" ){

Exceptions ex;

ex.code = Non_Real;

ex.msg = str;

throw ex;

}

return a;

}

Page 365: PROGRAMOZÁSEz 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

365

23. Feladat: Melyikből hány van

Olvassunk be a billentyűzetről egész számokat, majd mondjuk meg, melyik

szám hányszor szerepelt.

Specifikáció

A billentyűzetről érkező egész számok sorozatára a specifikációban a t

változóval hivatkozunk. Célunk egy olyan s sorozat, egy úgynevezett tároló

előállítása, amely érték-darabszám párokat tartalmaz, ahol egy érték egy t-

beli elem, a hozzátartozó darabszám pedig annak t-beli előfordulási száma.

Ugyanaz a t-beli elem csak egyszer szerepelhet az s-ben.

A = ( t : ℤ*, s : Pár* ) Pár = rec(érték : ℤ , darab : ℕ)

Ef = ( t=t’ )

Uf = ( Ef s = k

t

kt '

'

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 e részfeladat, amelynek

specifikációja:

A = ( s : Pár*, e : ℤ )

Ef = ( s=s’ e=e’ )

Uf = (e=e’ ).(,

'

1eértéksindl i

s

i

search

( l sind.darab = s’ind.darab+1) (l s = s’<n,1>) )

Absztrakt program

s := <>

k = 1 .. t k:ℕ

s := s tk

Page 366: PROGRAMOZÁSEz 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

366

s:=s e

l, i := hamis, 1 l : 𝕃, i:ℕ

l i ≤s

l := si.érték = e

ind := i ind:ℕ

i := i+1

l

sind.darab := sind.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

9-5. ábra. Alprogramok hívási lánca

main() Store()

Write()

Page 367: PROGRAMOZÁSEz 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

367

A main függvény az absztrakt főprogramot tartalmazza, amely a

billentyűzetről olvassa a képzeletbeli t sorozat elemeit, elhelyezi azokat az s-

ben a Store() segítségével, majd kiírja az s tartalmát a Write()-tal. A

Store() valósítja meg az s:=s e alprogramot, a bemenetként kapott e

elemet elhelyezi az s sorozatban. A Write() az s elemeinek a konzolablakra

történő kiírását végzi.

Komponens szerkezet

A függvényeket két csomagban helyezzük el. Külön csomagot alkot a main

függvény – ez lesz a main.cpp-ben –, és külön csomagba kerül a tárolót

megvalósító kód. Mivel magát az s tárolót nem exportáljuk, a container.h

fejállomány ezért csak a két művelet (Store() és Write()) deklarációját

tartalmazza. Az s típusának definíciója és a két művelet alprogramjának

törzse a container.cpp állományba kerül.

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.

main.cpp

main()

container.h - container.cpp

Store()

Write()

Page 368: PROGRAMOZÁSEz 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

368

Főprogram

A main függvény az absztrakt főprogramot kódolja:

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();

return 0;

}

Tároló csomag

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;

Page 369: PROGRAMOZÁSEz 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

369

};

static vector<Pair> s;

Amennyiben a static kulcsszót nem alkalmaznánk, akkor

módunkban állna, hogy a main.cpp-ben egy extern vector<Pair> s

deklarációt követően közvetlenül hozzáférjünk az s adataihoz. Ezt azonban

szeretnénk elkerülni, és csak a Store() és a Write() műveleteken

keresztül akarjuk használni az s sorozatot.

A Store() megvalósítása a programterv s:=s e alprogramja alapján

készül. Vegyük észre, hogy az eljárás feje nem utal arra, hogy ez az alprogram

az s sorozatot be és kimenő adatként egyaránt használja. Ez az egyik oka

annak, hogy a most bemutatott implementációs technikát nem tartjuk

kielégítőnek, helyette inkább objektum orientált nyelvi eszközt, az osztályt

használjuk szívesebben.

void Store(int e)

{

bool l = false;

int ind;

for(int i=0; !l && i<(int)s.size(); ++i){

l = s[i].value == e;

ind = i;

}

if(l) ++s[ind].no;

else{

Pair p; p.value = e; p.no = 1;

s.push_back(p);

Page 370: PROGRAMOZÁSEz 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

370

}

}

A Write() művelet a tároló elemeit kiíró eljárás.

void Write()

{

for(int i=0; i<(int)s.size(); ++i){

cout << s[i].value << " "

<< s[i].no << endl;

}

}

Tesztelés

Fekete doboz tesztesetek: (Az egyes programozási tételek tesztjét

összevonjuk a tételekben szereplő intervallum tesztjével.)

1. A main()-beli összegzés tesztje:

a. Egyetlen számot sem adunk meg.

b. Több számot adunk meg.

2. A Store()-beli lineáris keresés tesztje:

a. Egymás után két különböző számot adunk meg.

b. Egymás után két vagy több azonos számot adunk meg.

c. Nem közvetlenül egymás után adunk meg két azonos

számot.

Modulonkénti tesztelés:

Store()- Az előző tesztesetek lefedik a modul tesztelését (első elem

elhelyezése, a tároló tömbben még nem szereplő elem elhelyezése, létező

Page 371: PROGRAMOZÁSEz 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

371

elem ismételt elhelyezése a tároló tömb elején illetve végén), ugyanakkor a

modul nem kezeli a memória elfogyásnál bekövetkező eseményt.

Write()- Az eddigi tesztesetek lefedik ennek a modulnak a tesztelését.

Teljes program

main.cpp:

#include <iostream>

#include <fstream>

#include <vector>

#include "container.h"

using namespace std;

int main()

{

cout << "Adjon meg egész számokat!\n”

<< "Betűvel jelezze a bevitel végét!\n";

int n;

while(cin >> n){

Store(n);

}

cout << "\nA számok és az előfordulásuk:\n";

Write();

Page 372: PROGRAMOZÁSEz 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

372

return 0;

}

Page 373: PROGRAMOZÁSEz 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

373

container.h:

#ifndef CONTAINER_H

#define CONTAINER_H

#include <vector>

void Store(int e);

void Write();

#endif

container.cpp:

#include "container.h"

#include <iostream>

using namespace std;

struct Pair{

int value;

int no;

};

static vector<Pair> s;

Page 374: PROGRAMOZÁSEz 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

374

void Store(int e)

{

bool l = false;

int ind;

for(int i=0; !l && i<(int)s.size(); ++i){

l = s[i].value == e;

ind = i;

}

if(l) ++s[ind].no;

else{

Pair p; p.value = e; p.no = 1;

s.push_back(p);

}

}

void Write()

{

for(int i=0; i<(int)s.size(); ++i){

cout << s[i].value << " "

<< s[i].no << endl;

}

}

C++ kislexikon

Page 375: PROGRAMOZÁSEz 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

375

állomány-őrszem #ifndef NEV

#define NEV

#endif

csomagra

korlátozott globális

változó

static int x;

fejállomány exportált típusok, függvények deklarációi, esetleg

változók

forrásállomány 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.

Page 376: PROGRAMOZÁSEz 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

376

10. Rekurzív programok kódolása

Ez a fejezet egy kakukktojás. Könyvünk első kötetében, a tervezésnél, ugyanis

nem eset szó a rekurzív programokról, ezért rekurzív programok kódolásáról

sem beszélhetnénk. (Ne tévesszen meg bennünket az, hogy az első kötetben

sokat foglalkoztunk az intervallumon értelmezett rekurzív függvényekkel leírt

feladatok megoldásáról, amelyeket azonban nem rekurzív programokkal

oldottuk meg.) Másfelől, ha egy programban lehetőség van alprogramok

hívására, akkor előbb utóbb felvetődik a kérdés, hogy meghívhatja-e egy

alprogram önmagát, azaz lehet-e rekurzív alprogram-hívást alkalmazni. Ha

ezt megengedjük, akkor rekurzív alprogramról (rekurzív módon hívható,

rekurzívan definiált alprogramról) beszélünk. A rekurzív program pedig egy

rekurzív alprogramokat tartalmazó program.

Számos olyan feladat van, amelynek érthetőbb, jobban áttekinthetőbb

megoldása adható meg egy rekurzív programmal. A rekurzív programok

tervezése azonban nem egyszerűbb a hagyományos programoknál. Nem

kívánunk itt részleteiben foglalkozni a rekurzív programok helyességének és

tervezésének kérdéseivel, de gondoljunk csak a tervezés azon sarkalatos

pontjára, amely keretében a program leállását vizsgáljuk. Eddig csak a

ciklusok okozhattak az operációs rendszer által fel nem ismerhető végtelen

működést, rekurzív programoknál garanciát kell tudnunk mutatni arra, hogy

egy alprogram legfeljebb véges sokszor hívhatja meg közvetve vagy

közvetlenül önmagát.

Ebben a fejezetben néhány C++ nyelven kódolt rekurzív programot

mutatunk be, mivel ezek az eddig látott nyelvi elemeken kívül mást nem

igényelnek. A konkrét feladatoknál kitérünk majd az felhasznált algoritmusok

helyességének vizsgálatára is, anélkül, hogy általánosságban foglakoznánk

helyesség kérdésével.

Implementációs stratégia

Mindig a tervezési fázisban dől el az, hogy egy feladat megoldására

rekurziót alkalmazunk-e, illetve hogy azt rekurzív programmal

kódoljuk-e. Itt rögtön különbséget kell tudnunk tenni három fogalom

Page 377: PROGRAMOZÁSEz 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

377

között. Egy feladat leírása, specifikálása is történhet rekurzív

függvények segítségével (erre láthattunk példát az első kötetben),

adhatunk egy feladatra rekurzív algoritmus képében absztrakt

megoldást, és kódolhatjuk azt annak szerkezetéhez hűen ragaszkodva

rekurzív formában, de át is alakíthatjuk azt nem-rekurzív algoritmussá.

Például egy természetes szám faktoriálisát kiszámíthatjuk

összeszorzásokkal az összegzés programozási tétele alapján (lásd 6.

fejezet), ahol sem a megoldás ötlete, sem a megoldó algoritmus, sem

annak kódja nem tartalmaz rekurziót. Támaszkodhatunk azonban az

ismert n! = n*(n–1)! (és 1! = 1) rekurzív képletre is, amelyre

alkalmazhatjuk a rekurzív függvény értékét kiszámító, azaz nem-

rekurzí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)

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

n>1

f := n*Fact(n-1) f := 1

Page 378: PROGRAMOZÁSEz 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

378

hívása után bizonyosan a rekurziómentes ágra fusson rá a vezérlés. A

fenti alprogram minden egyes hívásával eggyel kisebb értéket ad az n

paraméterváltozónak. Ez a garancia arra, hogy véges számú hívás után

az 1 értékkel hívódik meg az alprogram, amikor is az már nem fog

újabb rekurzív hívást kezdeményezni.

Egy rekurzív alprogram lokális változójának annyi példánya lesz,

ahányszor az alprogramot meghívják. A fenti példában nincs értelme

feltenni azt a kérdést, hogy mennyi az n változó értéke, mert nem

beszélhetünk egyetlen n változóról. A vezérlés szempontjából azonban

mindig pontosan megállapítható az alprogram legutoljára meghívott

és még be nem fejezett változatának változói, és ezek értéke

egyértelműen meghatározható.

Minden rekurzív program átalakítható nem-rekurzív programmá

(sőt fordítva is). Az átalakítást többnyire a hatékonyság javításának

céljából tesszük meg, bár ilyenkor azt is mérlegelni kell, hogy az így

nyert program átláthatósága, és ennek következtében az érthetősége,

javíthatósága mennyire romlik a rekurzív változathoz képest. Egy

rekurzív program nagy előnye ugyanis a tömör, áttekinthető leírás.

Vannak olyan programozási nyelvek is, amelyek nem támogatják a

rekurzív alprogramok készítését, és ilyenkor a nem-rekurzív átalakítás

az egyetlen járható út.

Nyelvi elemek

A rekurzív alprogramok kódolásának nyelvi eszközei ugyanazok, mint

amelyeket a közönséges alprogramok kódolásánál megismertünk.

Fel kell azonban hívni még egyszer arra a figyelmet, hogy egy

alprogram minden hívása után a lokális változói újból létrejönnek és az adott

hívás befejeződéséig a verem memóriában maradnak. Az ismétlődő rekurzív

hívások ugyanazon lokális változók újabb és újabb példányait hozzák létre a

verem memóriában. Az új lokális változóknak a név- és típusegyezésen kívül

semmi kapcsolatuk sincs a korábbi meghívásakor létrejött lokális változókkal.

A verem memóriában egyidejűleg léteznek az alprogram korábbi és újabb

Page 379: PROGRAMOZÁSEz 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

379

hívása során létrejön lokális változók. Az újbóli hívás számára a korábbi hívás

azonos nevű lokális változói nem érhetőek el, azok értékét – ha szükség

lenne rájuk – paraméterátadással kell továbbadni.

Az ismétlődő rekurzív hívásokat tartalmazó program működésének

egyik veszélye, hogy a rendelkezésre álló memória gyorsan elfogyhat. Ezért

gondosan meg kell válogatni, hogy melyek legyenek egy rekurzív alprogram

lokális változói. A nagy méretű adatokat tartalmazó változókat célszerű

hivatkozás szerint átadni, vagy azt megfontolni, hogy egy-egy ilyen változó

kiemelhető-e globális változóvá a rekurzív alprogramból.

Page 380: PROGRAMOZÁSEz 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

380

24. Feladat: Binomiális együttható

Számoljuk ki a k-adik n-ed rendű binomiális együtthatót, az ( )-t!

Specifikáció

A feladat specifikációja igen egyszerű.

A = ( n,k,b : ℕ )

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 ℕ: 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 ℕ: ( ) (

) és

n2, k[1..n–1]: ( ) (

)

Page 381: PROGRAMOZÁSEz 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

381

A másik definíció az úgynevezett Pascal háromszög törvényszerűségét

használja ki:

n ℕ: ( ) (

) és

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

Page 382: PROGRAMOZÁSEz 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

382

b := 1 b := Binomial(n–1,k–1)+Binomial(n–1,k)

Igaz, hogy ennek idő hatékonysága rosszabb, az ( ) kiszámolásához akár 2

n-

szer is meghívódik az alprogram, ráadásul többször ugyanazon

paraméterekkel, de cserébe nem igényel sem szorzást, sem osztást, és a

részeredmény túlcsordulása sem következhet be.

Implementálás

Komponens szerkezet

A binomiális együttható kiszámolását a main függvényben találjuk. Itt

felhasználjuk a korábban már bemutatott ReadInt() függvényt a bemenő

adatok beolvasásához. Ezt a függvényt külön csomagban (read.h-

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

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

main.cpp

main()

Binomial()

read.h - read.cpp

ReadInt()

Nat()

main()

ReadInt()

Nat()

Binomial()

Page 383: PROGRAMOZÁSEz 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

383

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

Page 384: PROGRAMOZÁSEz 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

384

A fő program

A binomiális együttható kiszámolását ciklikusan ismételhető módon

valósítjuk meg a main függvényben.

int main()

{

char ch;

do{

int n = ReadInt("n= ",

"Természetes szám kell!\n",Nat);

int k = ReadInt("k= ",

"Természetes szám kell!\n",Nat);

cout << "B(n,k)= " << Binomial(n,k) << endl;

cout << "Folytatja? (I/N): "; cin >> ch;

}while(ch != 'n' && ch != 'N');

return 0;

}

Rekurzív függvény

A Binomial() függvényt a terv alapján kódoljuk.

int Binomial(int n, int k)

{

Page 385: PROGRAMOZÁSEz 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

385

if(0 == n || 0 == k || k == n) return 1;

else return Binomial(n-1,k-1)+Binomial(n-1,k);

}

Page 386: PROGRAMOZÁSEz 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

386

Tesztelés

Fekete doboz tesztesetek:

1. ( )=1

2. ( )=1

3. ( )=1

4. ( )=1

5. ( )=1

6. Általános esetek.

7. Skálázás. (Annak kimérése, hogy legfeljebb mekkora n és k értékekre

tudjuk kiszámolni az eredményt, és ez mennyire tart sokáig.)

Modul tesztek:

main()- A ciklikusan ismételhetőség kipróbálása.

ReadInt(),Nat()- Ezeket a függvényeket korábban már többször

használtuk, a tesztelésük ott megtalálható.

Binomial()- A fekete doboz tesztesetek lefedik a tesztelést.

Page 387: PROGRAMOZÁSEz 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

387

Teljes program

main.cpp:

#include <iostream>

#include "read.h"

using namespace std;

int Binomial(int n, int k);

int main()

{

char ch;

do{

int n = ReadInt("n= ",

"Természetes számot kérek!\n",Nat);

int k = ReadInt("k= ",

"Természetes számot kérek!\n",Nat);

cout << "B(n,k)= " << Binomial(n,k) << endl;

cout << "Folytatja? (I/N): "; cin >> ch;

}while(ch != 'n' && ch != 'n');

return 0;

}

Page 388: PROGRAMOZÁSEz 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

388

int Binomial(int n, int k)

{

if(0 == n || 0 == k || k == n) return 1;

else return Binomial(n-1,k-1)+Binomial(n-1,k);

}

read.h:

#ifndef READ_H

#define READ_H

#include <string>

bool ci(int k);

bool Nat(int n);

int ReadInt (std::string msg,

std::string errormsg,

bool cond(int) = ci);

#endif

read.cpp:

#include "read.h"

#include <iostream>

Page 389: PROGRAMOZÁSEz 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

389

using namespace std;

bool ci(int k){ return true;}

bool Nat(int n) { return n >= 0; }

int ReadInt(string msg,

string errormsg, bool cond(int) )

{

int n;

int hiba = true;

string tmp;

do{

cout << msg;

cin >> n;

if(cin.fail() || !check(n)){

cout << errmsg << endl;

cin.clear();

}

getline(cin,tmp);

}while(hiba);

return n;

}

Page 390: PROGRAMOZÁSEz 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

390

25. Feladat: Hanoi tornyai

Adjunk megoldást a Hanoi tornyai problémára! Ebben a játékban több

különböző méretű lyukas korong helyezkedik el három rúd valamelyikén.

Egyszerre mindig csak egy korong rakható át egy másik rúdra, de úgy, hogy

soha nem tehetünk nagyobb korongot kisebb korong tetejére. Kezdetben

minden korong az 1-es sorszámú rúdon található. Milyen mozgatásokkal

vihető át az összes korong a 3-as rúdra?

Specifikáció

Egy korong mozgatása két rúd-sorszámmal (honnan-hova) adható meg. Egy

ilyen mozgatást leíró (i,j) számpárt a továbbiakban a szemléletesség kedvéért

(i j) alakban fogjuk írni. A cél a megfelelő mozgatás-sorozat előállítása.

A = ( n : ℕ , ss : ( ℕ×ℕ )* )

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)

Page 391: PROGRAMOZÁSEz 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

391

Implementálás

A megoldó programot közvetlenül, rekurzívan hívható függvény segítségével

kódoljuk.

10-3. ábra. Alprogramok hívási láncai

Az alkalmazás szerkezete szinte szó szerint megegyezik az előző feladat

megoldáséval.

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

main()

ReadInt()

Nat()

Hanoi()

main.cpp

main()

Hanoi()

read.h - read.cpp

ReadInt()

Nat()

Page 392: PROGRAMOZÁSEz 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

392

cout << Hanoi(n,1,3,2) << endl;

cout << "Folytatja? (I/N): "; cin >> ch;

}while(ch != 'n' && ch != 'N');

return 0;

}

Rekurzív függvény

A Hanoi() függvényt a terv alapján kódoljuk. Az eredmény sztringet egy

ostringstream típusú objektumban állítjuk össze. Találkoztunk már ezzel a

cout-hoz hasonló adatfolyammal, amelybe betett adatok egy sztringbe

fűződnek fel, és ezt a sztringet tudjuk aztán az str() függvénnyel

lekérdezni.

string Hanoi(int n, int i, int j, int k)

{

ostringstream ss;

if(1 == n) ss << i << "->" << j;

else ss << Hanoi(n-1,i,k,j) << " , "

<< i << "->" << j << " , "

<< Hanoi(n-1,k,j,i);

return ss.str();

}

Tesztelés

Fekete doboz tesztesetek:

Page 393: PROGRAMOZÁSEz 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

393

1. Hanoi(1,1,3,2)

2. Hanoi(2,1,3,2)

3. Hanoi(3,1,3,2)

4. Általános eset.

5. Skálázás. (Annak kimérése, hogy legfeljebb mekkora n értékekre

tudjuk kiszámolni az eredményt, és ez mennyire tart sokáig.)

Modul tesztek:

main()- A ciklikusan ismételhetőség kipróbálása.

ReadInt(),Nat()- Ezeket a függvényeket korábban már többször

használtuk, a tesztelésük ott megtalálható.

Hanoi()- A fekete doboz tesztesetek lefedik a tesztelést.

Page 394: PROGRAMOZÁSEz 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

394

Teljes program

main.cpp:

#include <iostream>

#include <sstream>

#include "read.h"

using namespace std;

string Hanoi(int n, int i, int j, int k);

int main()

{

char ch;

do{

int n = ReadInt("n= ",

"Természetes számot kérek!\n",Nat);

cout << Hanoi(n,1,3,2) << endl;

cout << "Folytatja? (I/N): "; cin >> ch;

}while(ch != 'n' && ch != 'N');

return 0;

}

string Hanoi(int n, int i, int j, int k)

{

Page 395: PROGRAMOZÁSEz 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

395

ostringstream ss;

if(1 == n) ss << i << "->" << j;

else ss << Hanoi(n-1,i,k,j) << " , "

<< i << "->" << j << " , "

<< Hanoi(n-1,k,j,i);

return ss.str();

}

read.h:

#ifndef READ_H

#define READ_H

#include <string>

bool ci(int k);

bool Nat(int n);

int ReadInt(std::string msg,

std::string errmsg, bool cond(int)= ci);

#endif

read.cpp:

#include "read.h"

#include <iostream>

using namespace std;

Page 396: PROGRAMOZÁSEz 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

396

bool ci(int k){ return true;}

bool Nat(int n) { return n >= 0; }

int ReadInt(string msg,

string errmsg, bool cond(int) )

{

int n;

int hiba = true;

string tmp;

do{

cout << msg;

cin >> n;

if(cin.fail() || !cond(n)){

cout << errmsg << endl;

cin.clear();

}

getline(cin,tmp);

}while(hiba);

return n;

}

Page 397: PROGRAMOZÁSEz 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

397

26. Feladat: Quick sort

Implementáljuk a gyorsrendezést (quick sort)!

Specifikáció

A = ( v : ℤ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.

Page 398: PROGRAMOZÁSEz 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

398

a:= Divide(v,m,n)

x, a, f := v[n], m, n

a<f

v[a] ≤ x a<f

a := a+1

a<f

v[f], f := v[a], f-1 SKIP

v[f] ≥ x a<f

f := f-1

a<f

v[a], a := v[f], a+1 SKIP

v[a] := x

Ezt a Divide() függvényt hívja a gyorsrendezés rekurzív programja.

Quick(v,m,n)

m = n

SKIP

a := Divide(v,m,n)

Quick(v,m,a-1)

Quick(v,a+1,n)

Szokás az elágazás feltételét úgy enyhíteni, hogy ha m és n eltérése

már elég kicsi, akkor egy másik fajta (pl. beillesztéses) rendezéssel rendezzük

a tömb kijelölt szakaszát. Ezáltal a rendezés még gyorsabb lesz.

Implementálás

Page 399: PROGRAMOZÁSEz 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

399

A megoldó programot közvetlenül, rekurzívan hívható függvény segítségével

kódoljuk.

Komponens szerkezet

A rendezés algoritmusa a main.cpp állományban kerül kódolásra. Fontos

alprogramja a Quick() rekurzív eljárás, és az ebben hívott Divide()

függvény. A tömb szöveges állományból való feltöltését (Read() és a tömb

konzolablakba történő kiírását (Write()) külön csomagban (array.h-

array.cpp) helyezzük el.

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.

10-6. ábra. Alprogramok hívási láncai

A fő program

main.cpp

main()

Quick()

Divide()

array.h - array.cpp

Read()

Write()

main()

Read()

Write()

Quick() Divide()

Page 400: PROGRAMOZÁSEz 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

400

A main függvény hozza létre a rendezendő tömböt, kiírja azt a konzolablakba,

meghívja rá a gyorsrendezést, végül kiírja a rendezett alakot.

Page 401: PROGRAMOZÁSEz 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

401

int main()

{

vector<int> v;

Read(v);

cout << "Rendezés előtt: \n";

Write(v);

Quick(v,0,(int)v.size()-1);

cout << "Rendezés után: \n";

Write(v);

return 0;

}

Rekurzív eljárás

A Quick() eljárást, és a Divide() függvényt a terv alapján kódoljuk.

void Quick(vector<int> &v, int m, int n)

{

if( m < n ){

int a = Divide(v,m,n);

Quick(v,m,a-1);

Page 402: PROGRAMOZÁSEz 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

402

Quick(v,a+1,n);

}

}

int Divide(vector<int> &v, int a, int f)

{

int x = v[f];

while(a<f){

while( a<f && v[a]<=x ) ++a;

if( a<f ){ v[f] = v[a]; --f; }

while( a<f && v[f]>=x ) --f;

if( a<f ){ v[a] = v[f]; --a; }

}

v[a] = x;

return a;

}

Page 403: PROGRAMOZÁSEz 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

403

Tömb műveletek

Egy egydimenziós tömbbel kapcsolatban két műveletet vezetünk be.

Az egyik művelet egy szöveges állományból tölt fel egy tömböt. Az

állomány első adata a tömb elemszámát, az azt követő adatai pedig a tömb

elemeit tartalmazza. Feltesszük, hogy ezek mind egész számok, és a tömb

hosszát megadó szám sem negatív.

void Read(vector<int> &t)

{

ifstream f("input.txt");

if (f.fail()) {

cout << "Hibás fájlnév!\n";

exit(1);

};

int n;

f >> n;

t.resize(n);

for (int i=0;i<n;++i){

f >> t[i];

}

}

A másik művelet egy tömb elemeit a konzolablakba írja ki.

Page 404: PROGRAMOZÁSEz 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

404

void Write(const vector<int> &t)

{

for(int i=0; i<(int)t.size(); ++i) {

cout << "\t" << t[i];

}

cout << endl;

}

Tesztelés

Most csak a Quick() és a Divide() teszteléséhez elegendő fekete doboz

teszteseteket adjuk meg. A tesztelés többi részét az Olvasóra bízzuk.

1. Nulla hosszúságú tömb rendezése.

2. Egy elemű tömb rendezése.

3. Kettő-hatvány darab elemet tartalmazó tömb rendezése.

4. Kettő-hatványtól eltérő elemszámú tömb rendezése.

5. Csupa eltérő elemű tömb rendezése.

6. Csupa azonos elemű tömb rendezése.

7. Több azonos elemet is tartalmazó tömb rendezése.

8. Rendezett tömb rendezése.

Page 405: PROGRAMOZÁSEz 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

405

Teljes program

main.cpp:

#include <iostream>

#include "array.h"

using namespace std;

void Quick(vector<int> &v, int m, int n);

int Divide(vector<int> &v, int m, int n);

int main()

{

vector<int> v;

Read(v);

cout << "Rendezés előtt: \n";

Write(v);

Quick(v,0,(int)v.size()-1);

cout << "Rendezés után: \n";

Write(v);

return 0;

Page 406: PROGRAMOZÁSEz 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

406

}

void Quick(vector<int> &v, int m, int n)

{

if( m < n )

{

int a = Divide(v,m,n);

Quick(v,m,a-1);

Quick(v,a+1,n);

}

}

int Divide(vector<int> &v, int a, int f)

{

int x = v[f];

while(a<f){

while( a<f && v[a]<=x ) ++a;

if( a<f ){ v[f] = v[a]; --f; }

while( a<f && v[f]>=x ) --f;

if( a<f ){ v[a] = v[f]; --a; }

}

v[a] = x;

return a;

}

array.h:

Page 407: PROGRAMOZÁSEz 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

407

#ifndef ARRAY_H

#define ARRAY_H

#include <vector>

void Read(std::vector<int> &t);

void Write(const std::vector<int> &t);

#endif

array.cpp:

#include "array.h"

#include <fstream>

#include <iostream>

using namespace std;

void Read(vector<int> &t)

{

ifstream f("input.txt");

if (f.fail()) {

cout << "Hibás fájlnév!\n";

exit(1);

};

Page 408: PROGRAMOZÁSEz 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

408

int n;

f >> n;

t.resize(n);

for (int i=0;i<n;++i){

f >> t[i];

}

}

void Write(const vector<int> &t)

{

for(int i=0; i<(int)t.size(); ++i){

cout << "\t" << t[i];

}

cout << endl;

}

Page 409: PROGRAMOZÁSEz 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

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

Page 410: PROGRAMOZÁSEz 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

410

a megvalósításban egyaránt – egy sajátos adattípus központú, típus-orientált

szemlélet uralkodik. Ahogy a hátra levő fejezetekben egy-egy feladat

megoldásában egyre több osztály kerül bevezetésre és ezek egymáshoz való

viszonya is egyre érdekesebbé válik, úgy jelennek meg fokozatosan

programjainkban az objektum-orientált programozási stílus jegyei is. Nincs

azonban arról szó, hogy a korábbi módszerek, tapasztalatok feleslegessé

válnának, hiszen például egy-egy típusművelet megvalósítása az eddigi

(hagyományos, procedurális) szemlélet mentén történik. A két programozási

paradigma egymást erősítve, kiegészítve jelenik meg, és a megoldandó

feladat sajátosságán múlik, hogy melyik dominál a megoldó programban. Az

illusztrációként választott feladatok megoldásának megtervezésekor az első

kötet típus központú szemléletére támaszkodó módszereket használjuk, mert

az kiváló alapot ad az objektum-orientált programozási stílus bevezetéséhez

és természetes módon köti össze azt a procedurális programozási stílussal.

Újdonsága miatt a következő néhány fejezetben nyilvánvalóan az objektum-

orientá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.

Page 411: PROGRAMOZÁSEz 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

411

11. A típus megvalósítás eszköze: az osztály

Programjaink tervezésekor két féle adattípussal találkozhatunk. Olyanokkal,

amelyeknek van a választott programozási nyelvben megfelelője, illetve

olyanokkal, amelyeknek nincs. Ez utóbbiakat magunknak kell definiálni. Az

objektum-orientált programozási nyelvek ehhez a definícióhoz hathatós

segítséget nyújtanak egy különleges nyelvi elem formájában, amelyet

osztálynak nevezünk.

Implementációs stratégia

Tekintsünk el egyelőre attól, hogy egy objektum-orientált programozási

nyelvben az osztály milyen sokrétű, kifinomult nyelvi lehetőségeket biztosít a

programozó számára, és helyette vezessünk be egy végletekig

leegyszerűsített osztály fogalmat. Az osztályra olyan egységként gondoljunk,

amely változókat és alprogramokat tartalmaz, ahol az alprogramokkal a

változók értékeit kérdezhetjük le vagy változtathatjuk meg, illetve a változók

értékétől függő tevékenységet hajthatunk végre. Az osztálynak van egy neve,

amellyel azonosítani tudjuk.

Egy osztály példányosítása1 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.

Page 412: PROGRAMOZÁSEz 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

412

kizárólag az osztály leírásán belül, az osztály alprogramjaiban használhatjuk.

A metódus (tagfüggvény) egy osztályon belül definiált alprogram, amelyet az

osztály egy objektumával kapcsolatban lehet meghívni, és ezzel az

objektummal, illetve az objektum adattagjaival kapcsolatos tevékenységet

hajt végre.

Az osztály tökéletesen alkalmas eszköz arra, hogy nyelvi szinten leírjon

egy adattípust. Egy adat típusán az adat által felvehető típus-értékek

halmazát, az azokra megfogalmazott típus-műveleteket, típus-értékeinek

reprezentációját és a típus-műveleteket implementáló programokat értjük.

Ha ismert egy adattípus leírása, akkor ahhoz könnyen elkészíthető az azt

megvalósító osztály.

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

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.

Page 413: PROGRAMOZÁSEz 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

413

adattagokra sokszor fogalmazunk meg olyan megszorításokat, amelyek

együttesen a típus invariánsát teljesítik.

Az osztály egy metódusa felel meg a típus egy műveletének. A

metódusokat mindig az osztály egy objektumára hívjuk meg, azaz a

paraméterei között mindig szerepel legalább egy objektum. Tulajdonképpen

a metódusok egy objektummal kapcsolatos tevékenységeknek tekinthetők. A

helyesen megtervezett típus-műveletek kihasználják és megőrzik a művelet

által használt objektumok tagváltozóira vonatkozó típus-invariánst. Más

szavakkal, ha van egy olyan objektumunk, amely adattagjai kielégítik a típus-

invariá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.

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.

típus-értékek

típus-

reprezentáció

típus-invariáns

típus-műveletek

típus-műveletek

implementációi

class típus {

private:

adattagok

public:

konstruktor

metódusok

}

metódusok definíciói

típus osztály

Page 414: PROGRAMOZÁSEz 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

414

Az osztályokat tartalmazó programok tesztelése is újabb módszereket

kíván. Egy-egy osztály önálló komponensként jelenik meg egy alkalmazásban,

ezért azt önmagában is tesztelni kell. teszteljük az eddig látott módon az

egyes metódusok működését, de tesztelni kell a metódusok tetszőleges

variációinak hatását is. Egy komponens teszteléséhez érdemes külön

tesztkörnyezetet, egy menüt biztosító keretprogramot készíteni, mert

többnyire az a főprogram, amelyen keresztül igénybe vesszük egy

komponens szolgáltatásait, nem képes teljes körűen letesztelni azt.

Nyelvi háttér

Az előző pontban bevezetett osztály tulajdonképpen nem nyelvi, hanem

tervezési eszköz, a típus szinonimája, amelynek azonban a nyelvi szinten

történő megfogalmazása sem bonyolult. Kell egy kulcsszó (mondjuk class),

amely bevezeti az osztályt leíró blokkot, ezután megadjuk az osztály egyedi

nevét, majd a leírást tartalmazó blokkban felsoroljuk az adattagokat és a

metódusokat.

Ebben és a következő fejezetben feltételezzük, hogy az adattagok

mindegyikéhez automatikusan rendelődik memóriafoglalás (eddig amúgy is

csak ilyen változókkal találkoztunk). Ha nem ilyen lenne, akkor az onnan

ismerhető fel, hogy az adattagok között úgynevezett pointer-változó jelenik

meg, amelyhez explicit módon (new utasítással) kell a saját kódunkban

(sokszor a konstruktorban) memóriafoglalást létrehoznunk. A pointer

adattagok jelenléte lényeges vízválasztó az osztályok kezelésében. Egyelőre

mi az egyszerű osztályokkal foglalkozunk, amelyek nem igényelnek explicit

módon végzett dinamikus helyfoglalást.

Az alábbiakban egy osztály C++ nyelvi megvalósítását láthatjuk.

// Osztály definíciója

class Tipus {

private:

// adattagok

Tipus11 tag1;

Tipus12 tag2;

Page 415: PROGRAMOZÁSEz 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

415

public:

// konstruktorok

Tipus();

Tipus(…);

// destruktor

~Tipus();

// metódusok deklarációi

Tipus21 Metod1(…);

Tipus22 Metod2(…);

};

// Konstruktorok és a destruktor definíciói

Tipus::Tipus() {…}

Tipus::Tipus(…) {…}

Tipus::~Tipus(…) {…}

// Metódusok definíciói

Tipus21 Tipus::Metod1(…){…}

Tipus22 Tipus::Metod2(…){…}

Page 416: PROGRAMOZÁSEz 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

416

Ha adott egy Tipus nevű osztályunk, akkor a segítségével (annak

mintájára) létrehozhatunk objektumokat, amelyekre változók segítségével

hivatkozhatunk. Az alábbi esetben a t változó azonosít majd egy objektumot.

Tipus t

Egy osztály sajátosan jelöli ki a benne definiált tagok láthatóságát.

Alapértelmezés szerint egy osztály minden tagja privát, azaz rájuk csak az

osztályon belül, azaz az osztályt definiáló blokkban és az osztályhoz tartozó

metódusok törzsében lehet hivatkozni. Ez utóbbit azért hangsúlyozzuk, mert

egy metódus definíciója, azaz a metódus törzse nem feltétlenül az osztály

blokkján belül helyezkedik el. (Megjegyezzük, hogy ezzel enyhül a szokásos

láthatósági szabály, mely szerint egy azonosító láthatósága arra blokkra

terjed ki, amelyikben deklarálták.) A privát tagokat az egyértelműség

kedvéért private-ként jelöljük meg akkor is, ha ez a tulajdonság

alapértelmezett.

Ha azt szeretnék biztosítani, hogy egy tagváltozó vagy különösen egy

metódus az osztályon kívül is látható legyen, akkor ezt a public kulcsszó

segítségével jelölhetjük ki. A publikus tagok láthatósága kiterjed minden

olyan helyre, ahol maga az osztály látható. Egy objektum adattagjait

általában privátként szokták deklarálni, de ha mégis publikussá tesszük,

akkor a

t.tag1

formában lehetne rájuk hivatkozni. Ha viszont az elérhetősége privát marad,

akkor közvetlenül nem hivatkozhatunk rá az osztályon kívül, a fenti

kifejezésre fordítási hibát kapunk.

Ha egy metódust az osztály leíráson kívül akarjuk meghívni az osztály

egy objektumára, akkor a metódus láthatósága publikus kell legyen. Egy

metódus abban különbözik a szokásos alprogramtól, hogy csak az adott

osztály egy objektumával (ez a hívó objektum) lehet meghívni. A hívó

objektumnak a metódus neve előtt kell a hívásban szerepelnie:

t.Metod1(…)

Page 417: PROGRAMOZÁSEz 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

417

(A C++ nyelven később látunk majd p->Method1(…) alakú hívásokat

is, amennyiben a p egy objektumra mutató pointer-változó lesz.)

Amikor a metódust meghívják, akkor ez a hívó objektum (illetve annak

egy hivatkozása), aktuális paraméterként adódik át egy különleges formális

paraméterváltozónak. Ennek a neve többnyire kötött (C-szerű nyelvekben ez

a this, más nyelveken esetleg self), ezzel a névvel tudunk a hívó

objektumra hivatkozni a metódus törzsében. Sok nyelvben (C++, Java, C#,

Object Pascal) a metódus formális paraméterlistája ezt a változót explicit

módon nem is tartalmazza, ez egy alapértelmezett paraméterváltozó, de

van olyan nyelv is (Python), ahol fel kell tüntetni a formális

paraméterlistában.

A C++ nyelven a this alapértelmezett paraméterváltozó valójában

nem magát az objektumot, hanem csak egy arra mutató pointert jelöl. A

pointerekről a 13. fejezetben részletesen is lesz szó, egyelőre erről elég

annyit tudni, hogy emiatt egy metódusban a hívó objektumra *this

kifejezéssel, az objektum egy tagjára pedig a this->tag kifejezéssel

hivatkozhatunk, bár ez utóbbi helyett használhatjuk egyszerűen a tag

kifejezést is.

A metódusok törzsének helyét a programozási nyelvek eltérő módon

jelölik. C++ nyelven a metódus törzseket lehet az osztály definíción belül is,

de azon kívül is definiálni. Hatékonysági okból csak a rövid, néhány

értékadást tartalmazó metódus törzseket érdemes úgynevezett inline

definícióként, az osztály leírásban, közvetlenül a metódus deklarációja

mögött megadni. Összetettebb metódustörzs esetén a külső definíciót

ajánljuk. Ilyenkor a deklarációt meg kell ismételni, és a metódus neve elé az

osztályának nevét is le kell írni (Tipus::).

Egy osztálynak lehetnek privát metódusai is. (Ebben a tekintetben a

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.

Page 418: PROGRAMOZÁSEz 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

418

Azokat a metódusokat, amelyek nem változtatják meg a hívó

objektumukat konstans metódusoknak nevezik. C++ nyelven ezt a metódus

deklarációjának végén feltüntetett const szóval jelezhetjük.

Minden osztálynak vannak speciális metódusai: az osztály nevével

megegyező konstruktor (ebből több is lehet) és ennek ellentét párja, a

destruktor. A konstruktornak és a destruktornak nincs visszatérési típusa, és

ezt még jelölni sem kell (tehát nem írunk a deklarációjuk elejére void-ot

sem). Mindkettő automatikusan hívódik meg: a konstruktor akkor, amikor

egy objektumot létrehozunk, a destruktor akkor, amikor az objektum

élettartama lejár.

Destruktorból csak egy van, de konstruktorból több is lehet, amelyek

paraméterlistáinak a paraméterek számában, típusában vagy sorrendjében

különböznie kell. Ha elfelejtenénk definiálni konstruktort vagy destruktort,

akkor „hivatalból” lesz az osztálynak egy üres paraméterlistájú üres törzsű

alapértelmezés szerinti üres konstruktora illetve egy üres destruktora. Ha

explicit módon definiálunk egy konstruktort, akkor ez az alapértelmezett

konstruktort még akkor is törli, ha az új konstruktor paraméterlistája nem

üres. Ilyenkor, ha szükségünk van egy üres konstruktorra is, akkor azt explicit

módon definiálnunk kell.

Az alábbi osztálynak két konstruktora van.

class Tipus{

private:

int n;

std::string str;

public:

Tipus(){n = 0; str = "hello";}

Tipus(int a, std::string b)

{ n = i; str = b; … }

};

Page 419: PROGRAMOZÁSEz 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

419

Objektum létrehozásakor mindig az a konstruktor hajtódik végre,

amelyiknek formális paraméterlistája illeszkedik a létrehozásnál feltüntetett

aktuális paraméterlistához. A Tipus o forma alkalmazásakor az üres

paraméterlistájú konstruktor hívódik meg, a Tipus o(12, "hello")

esetén pedig az aktuális paraméterlista elemeinek száma, típusa és sorrendje

alapján beazonosítható konstruktor.

Az adattagok kezdeti értékadása az alábbi konstruktor segítségével is

elvégezhető. Ennek hatása egyenértékű a fenti mutatott osztály második

konstruktorával.

Tipus(int a, std::string b):n(i),str(b) { … }

A konstruktor feladata egyszerű osztály esetén az, hogy a létrehozandó

objektum adattagjaihoz a típus-invariánst kielégítő kezdőértéket rendelje.

A destruktor szerepe egyszerű osztályoknál az egyes adattagok

megfelelő lezárása, amennyiben ehhez nem elegendő az adattagok

destruktora, amelyek viszont automatikusan meghívódnak. Ennél fogva

egyszerű osztályoknál gyakori, hogy nem írunk destruktort.

Egy osztálynak az adattagokon és metódusokon kívül lehetnék még

egyéb tagjai is, mint konstansok, típusdefiníciók, belső osztályok stb.

Két különböző osztálynak lehet ugyanolyan nevű és típusú adattagja,

ugyanolyan nevű és típusú metódusa. Sőt egy osztályon belül lehetnek

azonos nevű metódusok is, ha azoknak eltér a típusa: paraméterlistája,

visszatérési érték típusa és attribútumai (private/public, static,

const). Ezt a többalakúságot az operátorokra is alkalmazhatjuk, azaz

például új, az adott osztály objektumaihoz köthető jelentés adható például a

+ operátornak. (Erre majd mutatunk példákat.)

Az osztály leírást a kódban kétféleképpen helyezhetjük el. Az első az,

amikor az osztályt használó forrásállomány elején definiáljuk az osztályt, és

ugyanebben a forrásállományban az egyéb függvény-definíciók között

definiáljuk az osztály metódusait. A másik az, amikor az osztály leírást külön

csomagba helyezzük. C++ nyelven az osztály-definíciót egy fejállományba, az

ahhoz tartozó metódus-definíciókat pedig az ahhoz tartozó forrásállományba

szoktuk tenni, és a fejállományt „beinklúdoljuk” mind a metódus-definíciókat

Page 420: PROGRAMOZÁSEz 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

420

tartalmazó forrásállományba, mind az összes olyan forrásállományba, ahol az

osztály szolgáltatásait használni akarjuk. Az összetartozó fejállomány-

forrá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ű.

Page 421: PROGRAMOZÁSEz 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

421

Osztállyal kapcsolatos speciális nyelvi fogalmak

Adattagok kezdeti értékadása

A konstruktorok definíciójában a formális paraméter listát követő kettőspont

után az adattagok kezdeti értékadásait hívhatjuk meg. Ehhez az érintett

tagváltozókat kell felsorolni úgy, hogy a nevük mögötti zárójelben adjuk meg

a nekik szánt kezdő értéket:

Tipus::Tipus(): tag1(…), tag2(…), … {…}

Értékadás operátor, másoló konstruktor

Minden osztály alapértelmezés szerint rendelkezik egy úgynevezett másoló

konstruktorral (amely egy már létező objektum adattagjainak értékeivel hoz

létre egy új objektumot) és egy értékadás operátorral (amely egy objektum

adattagjaival felülírja egy másik objektum adattagjait).

Tipus o2(o1); // Tipus o1 már létezik

o1 = o2;

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

Page 422: PROGRAMOZÁSEz 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

422

hatókörét kiterjesztő lehetőség.

Osztály szintű elemek

Egy osztályban lehetőség van (static jelző használatával) olyan adattagok

illetve metódusok definiálására is, amelyek az osztály alapján létrehozott

objektumoktól független elemek lesznek. A statikus adattagok többnyire az

osztállyal kapcsolatos statisztikai adatok tárolására (hány objektum jött létre)

alkalmasak. A statikus metódusoknak nincs alapértelmezett objektum

paraméterük, de valamilyen módon kötődnek az osztályukhoz (például annak

több objektumával végeznek műveletet). Ebben a tekintetben a lehetőségeik

a barát alprogramokhoz hasonlóak.

Page 423: PROGRAMOZÁSEz 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

423

27. Feladat: UFO-k

Képzeljük el, hogy egy űrállomást állítanak Föld körüli pályára, amelynek az a

feladata, hogy adott számú észlelt, de ismeretlen tárgy közül megszámolja,

hány tartózkodik az űrállomás közelében (mondjuk 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.

1.

A = ( g : Gömb, v : Pontn, db : ℕ )

Ef = ( g=g' v=v' )

Uf = ( Ef

giv

n

i

db

][1

1 )

Gömb típus:

gömb l:=pg

g:Gömb, p:Pont, l:𝕃

(c,r):Pontℝ,

r0

l:=távolság(c,p) r

p:Pont, d:ℝ, l:𝕃

Pont típus:

pont d:=távolság(p,q)

p,q:Pont, d:ℝ

(x,y,z): ℝℝℝ 222 zqzpyqypxqxpd )..()..()..(:

Absztrakt program

Page 424: PROGRAMOZÁSEz 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

424

Ezt a feladatot legfelső szinten egy számlálás oldja meg.

db := 0

i =1 .. n

v[i]g

db := db + 1 SKIP

Implementálás

Az implementáció magán viseli mind a funkció vezérelt, mind a típus

központú szemléletmódot. Mindezzel együtt ez már egy objektum orientált

kódot eredményez.

Komponens szerkezet

A megoldó program kódját négy részre vágjuk. Külön csomagot alkot a Gömb

típust (Sphere), külön a Pont típust (Point) megvalósító kód, külön

csomagba kerülnek az egész és valós számok beolvasását segítő függvények

(a ReadInt() és ReadReal() függvényekkel már a korábbi fejezetekben

találkoztunk), a főcsomag pedig a main függvényt tartalmazza. A sphere.h

és point.h csomagok kialakítása a típus orientáltság, a read.h-read.cpp

pedig a funkció orientáltság jegyében történt.

11-3. ábra. Komponens szerkezet

main.cpp

• main()

sphere.h

Sphere

• Sphere()

• In()

point.h

Point

• Point()

• Set()

• Distance()

read.h - read.cpp

ReadInt()

ReadReal()

Pos()

Page 425: PROGRAMOZÁSEz 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

425

Page 426: PROGRAMOZÁSEz 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

426

Függvények hívási láncolata

A vezérlést, azaz a komponensek egyes függvényeinek megfelelő sorrendben

történő meghívását a main függvény biztosítja. Minden hívás vagy egy gömb

(Sphere), vagy egy pont (Point) objektummal kapcsolatos tevékenységért

felelős.

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;

main()

ReadReal() Pos()

ReadInt()

Point()

Set()

Sphere()

In() Distance()

Page 427: PROGRAMOZÁSEz 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

427

}

bool In(const Point &p)

{ return c.Distance(p)<=r; }

};

A Gömb típust a Sphere osztály valósítja meg. Tekintettel a

metódusok egyszerű voltára, azokat inline módon, a definícióba ágyazva

implementáltuk, ezért nem tartozik a sphere.h állományhoz sphere.cpp

állomány is.

A konstruktor negatív sugár esetén kivételt dob. Az itt implementált

programban ez a kivétel ugyan nem következhet be, mert majd, mint látni

fogjuk, a sugár értékének beolvasásánál ellenőrzést végzünk.

Az In() metódus a Point osztály Distance() metódusát hívja,

annak segítségével vizsgálja meg, hogy a megadott pont mennyire esik közel

a gömb középpontjához.

Pont típus megvalósítása

A Pont típust a Point osztály valósítja meg. A metódusokat itt is inline

módon implementáljuk.

class Point{

private:

double x,y,z;

public:

Point(){ x = y = z = 0.0;}

void Set(double a, double b, double c)

{ x = a; y = b; z = c;}

double Distance(const Point &p) const {

Page 428: PROGRAMOZÁSEz 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

428

return sqrt(pow(p.x-x,2)+ pow(p.y-y,2)

+ pow(p.z-z,2));

}

};

A Point() konstruktor egy origóba pozícionált pontot hoz létre. Egy

pontnak a pozícióját a Set() metódus segítségével tudjuk megváltoztatni.

Felmerülhet a kérdés, miért nem definiálunk inkább olyan konstruktort

is, amelyik egyből beállítaná a létrehozandó pont koordinátáit. Ennek az a

magyarázata, hogy amikor majd egy vector<Point> típusú tömböt hozunk

létre a főprogramban, akkor ott a Point-nak csak egy paraméter nélküli

konstruktorával jöhetnek létre a tömb elemei, és csak ezután tudjuk

egyenként módosítani a tömbbeli pontok pozícióit. Önálló pontot csak

egyszer kell létrehozni (a gömb középpontját), ennek kedvéért most nem

definiálunk másik, koordinátákkal paraméterezhető konstruktort. (Ha majd

megismerjük a pointerváltozó fogalmát, akkor lehetőség nyílik az ittenitől

eltérő megoldásra is.)

A Distance() metódussal nem két pont távolságát, hanem egy

pontnak egy másiktól való távolságát számoljuk ki. E két értelmezés között az

eredmény szempontjából ugyan nincs semmi különbség, de a nyelvi

megvalósításukban már igen. Az első értelmezéshez jobban illeszkedne egy

Distance(p,q) alakú hívás, de ekkor a Distance() nem lehetne a Point

osztály metódusa, csak egy két Point típusú paraméterrel rendelkező

függvény. A nyelvi szabályok miatt a Point osztály csak olyan metódust

definiálhatunk, amelyet p.Distance(q) alakban hívhatunk meg, amely a

második értelmezést testesíti meg. A Distance()metódus konstans

metódus, hiszen nem változtatja meg azt a pontot, amelyiktől vett távolságot

számítja. A metódus törzse támaszkodik a cmath csomag szolgáltatásaira

(gyökvonás, hatványozás).

Főprogram kódolása

Page 429: PROGRAMOZÁSEz 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

429

A main függvény először az űrállomás (a gömb) középpontjának koordinátáit

olvassa be (ReadReal()), létrehozza a középpontot (Point()) és beállítja

(Set()) a koordinátáit, majd a sugár beolvasása után magát a gömböt is

megalkotja (Sphere()).

double x,y,z,r;

cout << "Az űrállomás koordinátái:\n";

x = ReadReal("\t x: ", "Valós számot várok!");

y = ReadReal("\t y: ", "Valós számot várok!");

z = ReadReal("\t z: ", "Valós számot várok!");

Point c; c.Set(x,y,z);

r = ReadReal("Űrállomás körzetének sugara: ",

"Nem-negatív valós szám kell!", Pos);

Sphere g(c,r);

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!");

Page 430: PROGRAMOZÁSEz 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

430

vector<Point> v(n);

for(int i=0; i<n; ++i){

cout << "Az " << i+1

<< "-dik UFO koordinátái:\n";

x = ReadReal("\tx: ","Valós számot várok!");

y = ReadReal("\ty: ","Valós számot várok!");

z = ReadReal("\tz: ","Valós számot várok!");

v[i].Set(x,y,z);

}

Az absztrakt főprogram egy egyszerű számlálás, amely a „benne van-e

egy pont a gömbben” műveletét hívja meg (In()). Ezt az eredmény kiírása

követi.

int db = 0;

for(int i=0; i<n; ++i){

if(g.In(v[i])) ++db;

}

cout << "Közeli UFO-k száma: " << db;

Page 431: PROGRAMOZÁSEz 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

431

Tesztelés

Fekete doboz tesztesetek: (a számlálás, annak intervalluma és különleges

értékek tesztelése)

1. Nulla darab pont esete.

2. Nulla sugarú gömb esete benne levő ponttal.

3. Olyan gömb és pontok, ahol az első pont/utolsó pont esik csak a

gömbbe.

4. Olyan adatok, hogy egyetlen pont sem esik a gömbbe.

5. Olyan adatok, hogy minden pont a gömbbe esik.

6. Általános eset.

7. Negatív sugarú gömb.

A komponensek (osztályok) tesztelése nem túl bonyolult, hiszen a

konstruktoron és értékadó (setter) metóduson kívül mindkét osztály egyetlen

metódust tartalmaz csak, így a metódusok variációinak kipróbálására nincs

szükség.

1. Sphere osztály tesztelése

a. A Negativ_Radius kivétel dobásának tesztelése.

b. Nulla sugarú gömb létrehozása.

c. Egy gömb és egy a gömbbe eső pont vizsgálata.

d. Egy gömb és egy a gömbön kívüli pont vizsgálata.

e. Egy gömb és egy a gömb felületére eső pont vizsgálata.

2. Point osztály tesztelése.

a. Pont létrehozása.

b. Pont pozíciójának megváltoztatása

c. Két pont távolsága (azonos, csak egy koordinátában eltérő,

csak két koordinátában eltérő, mindhárom koordinátában

eltérő pontokkal)

d. A p és q pontok, illetve q és p pontok távolsága megegyezik-e

(szimmetria)?

Page 432: PROGRAMOZÁSEz 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

432

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

következik.

Page 433: PROGRAMOZÁSEz 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

433

Teljes program

main.cpp:

#include <iostream>

#include <vector>

#include "read.h"

#include "point.h"

#include "sphere.h"

using namespace std;

int main()

{

// Űrállomás beolvasása

double x,y,z,r;

cout << "Az űrállomás koordinátái:\n";

x = ReadReal("\t x: ", "Valós számot várok!");

y = ReadReal("\t y: ", "Valós számot várok!");

z = ReadReal("\t z: ", "Valós számot várok!");

Point c; c.Set(x,y,z);

r = ReadReal("Űrállomás körzetének sugara: ",

"Nem-negatív valós számot várok!", Pos);

Sphere g(c,r);

// UFO-k beolvasása

int n = ReadInt("UFO-kszáma: ",

Page 434: PROGRAMOZÁSEz 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

434

"Természetes számot várok!");

vector<Point> v(n);

for(int i=0; i<n; ++i){

cout << "Az " << i+1

<< "-dik UFO koordinátái:\n";

x = ReadReal("\tx: ","Valós számot várok!");

y = ReadReal("\ty: ","Valós számot várok!");

z = ReadReal("\tz: ","Valós számot várok!");

v[i].Set(x,y,z);

}

// Számlálás

int db = 0;

for(int i=0; i<n; ++i){

if(g.In(v[i])) ++db;

}

// Kiírás

cout << "Közeli UFO-k száma: " << db;

char ch; cin>>ch;

return 0;

}

sphere.h:

#ifndef SPHERE_H

#define SPHERE_H

Page 435: PROGRAMOZÁSEz 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

435

#include "point.h"

class Sphere{

private:

Point c;

double r;

public:

enum Errors { Negativ_Radius };

Sphere(const Point &p, double a) {

if (a<0) throw Negativ_Radius; c=p; r=a;

}

bool In(const Point &p)

{ return c.Distance(p)<=r; }

};

#endif

point.h:

#ifndef SPHERE_H

#define SPHERE_H

Page 436: PROGRAMOZÁSEz 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

436

#include <cmath>

class Point{

private:

double x,y,z;

public:

Point(){ x = y = z = 0.0;}

void Set(double a, double b, double c)

{ x = a; y = b; z = c;}

double Distance(const Point &p) const {

return sqrt(pow(p.x-x,2)+ pow(p.y-y,2)

+ pow(p.z-z,2));

}

};

#endif

Page 437: PROGRAMOZÁSEz 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

437

read.h:

#ifndef _READ_H

#define _READ_H

#include <string>

bool All(int n){ return true; }

bool All(double r){ return true; }

bool Pos(double a){ return a>=0.0;}

int ReadInt (std::string msg, std::string errormsg,

bool cond(int) = All);

double ReadReal(std::string msg,

std::string errormsg,

bool cond(double) = All);

#endif

read.cpp:

#include "read.h"

#include <iostream>

using namespace std;

int ReadInt(string msg, string errmsg,

bool check(int) )

{

int n;

int error = true;

do{

cout << msg; cin >> n;

Page 438: PROGRAMOZÁSEz 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

438

if(error = cin.fail() || !check(n)){

cout << errmsg << endl;

cin.clear();

}

string tmp; getline(cin,tmp);

}while(error);

return n;

}

double ReadReal(string msg, string errmsg,

bool check(double))

{

double a;

bool error = true;

do{

cout << msg; cin >> a;

if(error = cin.fail() || !check(n)){

cout << errmsg << endl;

cin.clear();

}

string tmp; getline(cin,tmp);

}while(error);

return a;

}

Page 439: PROGRAMOZÁSEz 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

439

28. Feladat: Zsák

Adott egy n, de legalább 1 hosszúságú legfeljebb 0 és 99 közé eső egész

számokat tartalmazó tömb. Melyik a tömbnek a leggyakrabban előforduló

eleme!

Specifikáció

A feladat részletes elemzésével már találkoztunk (I. kötet 7.2 példa), most

csak annak eredményét idézzük fel.

A = ( t : {0..99} n, b : Zsák, e : {0..99} )

Ef = ( t = t’ n≥1 )

Uf = ( Ef n

i

itb

1

]}[{

e = MAX(b) )

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.

Page 440: PROGRAMOZÁSEz 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

440

b :=

i =1..n

b := b{t[i]}

e := MAX(b)

Implementálás

Az implementálás egy objektum orientált kódot eredményez.

Komponens szerkezet

A megoldásban külön csomagba kerül a Zsák típus (Bag) és külön csomagba a

főprogram, amely a main függvény mellett a feladatban szereplő bemeneti

tömb feltöltését végző Read() függvényt is tartalmazza.

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.

main.cpp

• main()

• Read()

bag.h - bag.cpp

Bag

• Bag()

• Put()

• Max()

Page 441: PROGRAMOZÁSEz 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

441

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

};

main()

Read()

Bag()

Put()

Max()

Page 442: PROGRAMOZÁSEz 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

442

A reprezentációban bevezetjük a 100-as értéket helyettesítő

konstanst. Ennek az előnye az, hogy amennyiben ez az érték változna, akkor

ezt elég egyetlen helyen módosítani. Ezt egy statikus konstansként hozzuk

létre, így ez nem egyetlen zsákra, hanem a Bag osztályból létrehozható

összes zsákra (bár ebben a feladatban csak egyetlen zsákot használunk)

egyaránt érvényes.

A metódusok megvalósítását elkülönítve a bag.cpp állományba

helyezzük el. A metódusok törzse a terv alapján készült, kiegészülve a

kivételek dobásával. A Put() metódus akkor dob WrongInput kivételt, ha

nem 0 és 99 közötti számot akarunk a zsákba betenni. A Max() konstans

metódus akkor dob EmptyBag kivételt, ha a zsák üres.

#include "bag.h"

using namespace std;

Bag::Bag() { for (int k=0; k<n; ++k) v[k] = 0; }

void Bag::Put(int e)

{

if (e<0 || e>n-1) throw WrongInput;

++v[e];

}

int Bag::Max() const

{

int max = v[0];

int e = 0;

for (int k=1; k<n; ++k)

Page 443: PROGRAMOZÁSEz 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

443

if (v[k]> max){ max = v[k]; e = k; }

if (0 == max) throw EmptyBag;

return e;

}

Főprogram kódolása

A main függvény először deklarálja a t bementi tömböt és létrehozza a

zsákot, majd feltölti a bemeneti tömböt a Read() függvény segítségével.

Megjegyezzük, hogy a tömb a megvalósításban 0-tól indexelt. Ezt követően a

tömb elemeit egyenként betesszük a zsákba. Az utolsó lépés a leggyakoribb

elem kiválasztása és kiírása.

Mivel előfordulhat, hogy a zsákba betenni kívánt elem nem 0 és 99

közé esik, ezért a Put() műveletet kivételkezelésnek vetjük alá. Lekezeljük

az EmptyBag kivételt is, amely akkor következhet be, ha a bemeneti tömb

üres volt vagy nem tartalmazott 0 és 99 közötti elemet, azaz a zsák üres

maradt. Ezen a ponton általánosítottuk a tervet, hiszen ott feltettük, hogy a

bemeneti tömb nem üres és csupa 0 és 99 közötti számot tartalmaz.

A bementi tömb feltöltése egy olyan szöveges állományból történik,

amelyik első adata a tömb elemszámát, az azt követő adatai pedig a tömb

elemeit tartalmazza. Feltesszük, hogy ezek mind egész számok, és a tömb

hosszát megadó szám sem negatív.

vector<int> t;

Read(t);

Bag b;

for(int i=0;i<(int)t.size();++i){

try{ b.Put(t[i]); }

Page 444: PROGRAMOZÁSEz 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

444

catch(Bag::Errors ex){

if(Bag::WrongInput == ex)

cout << "Hibás adat a tömbben!\n";

}

}

try{ cout << "Leggyakoribb elem: " << b.Max();}

catch(Bag::Errors ex){

if(Bag::EmptyBag == ex)

cout << "Üres tömb!\n";

}

Page 445: PROGRAMOZÁSEz 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

445

Tesztelés

Fekete doboz tesztesetek:

Érvénytelen esetek:

1. Üres bemeneti tömb

2. Egyetlen, nem 0 és 99 közötti számot tartalmazó bementi tömb.

3. Több, nem 0 és 99 közötti számot tartalmazó bementi tömb.

4. Csupa, nem 0 és 99 közötti számot tartalmazó bementi tömb.

Érvényes esetek:

1. Egyetlen 0 és 99 közötti számot tartalmazó bementi tömb.

2. Olyan bemeneti tömb, amely első/utolsó eleme a nulla, ezen kívül

még egy nullát tartalmaz, minden más számból legfeljebb egyet.

3. Olyan bemeneti tömb, amely első/utolsó eleme a 99, ezen kívül még

egy 99-t tartalmaz, minden más számból legfeljebb egyet.

4. Olyan bemeneti tömb, amelyben több szám is egyforma

gyakorisággal található.

A főprogram fehérdoboz tesztelése sem igényel újabb teszteseteket, a

bemeneti tömb feltöltése, az elemek zsákba pakolása a fekete doboz

tesztesetekkel már ki lett próbálva.

Komponens teszt. Külön tesztelendő a Bag osztály. Elsősorban a Max()

művelet tesztelésére kell figyelnünk (amikor a zsák első vagy utolsó eleme a

legnagyobb számosságú, vagy több egyformán maximális számosság is van),

de ezt a fenti érvényes esetek lefedik. Ezen kívül a kivétel dobásokat és azok

kezelését kell még ellenőrizni. A metódusok variációinak tesztje itt nem kell.

Page 446: PROGRAMOZÁSEz 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

446

Teljes program

main.cpp:

#include <iostream>

#include <fstream>

#include <vector>

#include <cstdlib>

#include "bag.h"

using namespace std;

void Read(vector<int> &t);

int main()

{

vector<int> t;

Read(t);

Bag b;

for(int i=0;i<(int)t.size();++i){

try{ b.Put(t[i]); }

catch(Bag::Errors ex){

if(Bag::WrongInput == ex)

cout << "Hibás adat a tömbben!\n";

}

}

Page 447: PROGRAMOZÁSEz 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

447

try{ cout << "Leggyakoribb elem: " << b.Max();}

catch(Bag::Errors ex){

if(Bag::EmptyBag == ex)

cout << "Üres tömb!\n";

}

char ch; cin >> ch;

return 0;

}

void Read(vector<int> &t)

{

ifstream f("input.txt");

if (f.fail()) {

cout << "Hibás fájlnév!\n";

exit(1);

};

int n;

f >> n;

t.resize(n);

for (int i=0;i<n;++i){

f >> t[i];

}

}

bag.h:

#ifndef BAG_H

Page 448: PROGRAMOZÁSEz 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

448

#define BAG_H

#include <vector>

class Bag{

public:

enum Errors{WrongInput, EmptyBag};

Bag();

void Put(int e);

int Max() const;

private:

static const int n = 100;

int v[n];

};

#endif

bag.cpp:

#include "bag.h"

using namespace std;

Page 449: PROGRAMOZÁSEz 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

449

Bag::Bag()

{

for (int k=0; k<n; ++k) v[k] = 0;

}

void Bag::Put(int e)

{

if (e<0 || e>n-1) throw WrongInput;

++v[e];

}

int Bag::Max() const

{

int max = v[0];

int e = 0;

for (int k=1; k<n; ++k)

if (v[k]> max){ max = v[k]; e = k; }

if (0 == max) throw EmptyBag;

return e;

}

Page 450: PROGRAMOZÁSEz 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

450

29. Feladat: Síkvektorok

Adott n+1 darab síkvektor. Igaz-e, hogy az első n darab síkvektor összege

merőleges az n+1-edik síkvektorra?

Specifikáció

A feladat megoldásához ki kell számolnunk az n darab síkvektor

eredőjét (összegzés), majd ennek az n+1-edik vektorral vett skaláris

szorzatát. Ha ez nulla, akkor merőleges az eredő az n+1-edik síkvektorra.

A = ( t : Síkvektorn, v : Síkvektor , l : 𝕃 )

Ef = ( t=t' v=v' )

Uf = ( Ef

n

i

s

1

1

l = (s*v=0.0) )

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() a,b: ℝ

v := v + v2 v2:Síkvektor

d := v1 * v2 v1,v2:Síkvektor, d: ℝ

(x, y): ℝℝ x, y := 0.0, 0.0

x := a; y := b a,b: ℝ

x,y := x + v2.x, y + v2.y

d := v1.x*v2.x + v1.y*v2.y d: ℝ

Page 451: PROGRAMOZÁSEz 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

451

Absztrakt program

A feladatot egy összegzés, majd a skaláris szorzás eredményének

vizsgálata oldja meg.

s := nullvektor

i =1 .. n

s := s + t[i]

l := s*v=0.0

Implementálás

Az implementálás során elkészítjük a Síkvektor típust, majd kódoljuk a fenti

absztrakt programot.

Komponens szerkezet

A megoldó program kódját három részre vágjuk. Külön csomagot alkot a

Síkvektor típusát megvalósító Vector2D osztály, külön csomagba kerülnek

az egész, a természetes és a valós számok beolvasását segítő függvények (a

ReadInt() és ReadReal() függvények), a főprogram pedig a main

függvényt tartalmazza.

A Vector2D osztály operator+=() metódusa a v := v + v2 hozzáadás

műveletét, az operator*() pedig a d := v1 * v2 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.

Page 452: PROGRAMOZÁSEz 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

452

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.

11-8. ábra. Alprogramok hívási lánca

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

main()

Vector2D()

Fill()

ReadInt() Nat()

ReadReal()

SetX(), SetY()

Sum() Vector2D()

operator+=() ReadReal()

Vector2D(,)

operator*()

Page 453: PROGRAMOZÁSEz 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

453

Főprogram kódolása

A main függvény először a síkvektorok tömbjét hozza létre, amelyet a

Fill() függvény segítségével tölt fel, majd a Sum() függvény segítségével

kiszámolja a síkvektorok összegét. A külön síkvektor beolvasása illetve

létrehozása után pedig ennek és az összegnek a skaláris szorzatát számolja ki,

és az eredményt összeveti a 0.0-val.

cout << "Összeadandó vektorok:" << endl;

vector<Vector2D> t;

Fill(t);

Vector2D s = Sum(t);

cout << "Külön vektor:" << endl;

Vector2D v( ReadReal("x = "), ReadReal("y = "));

if(s*v == 0.0) cout << "Merőleges";

else cout << "Nem merőleges.";

A Fill() eljárás megadott méretre módosítja a paraméterként

kapott tömböt, majd – mivel ebben csupa nullvektor van – a felhasználó

adatai alapján módosítja a tömb összes síkvektorának koordinátáit. A

ReadReal() alprogram olyan megvalósítását igényli az alábbi kód, amelyik

második és harmadik paramétere is rendelkezik alapértelmezett értékkel, így

hívásakor elég csak az első paraméterét megadni.

void Fill(vector<Vector2D> &t)

Page 454: PROGRAMOZÁSEz 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

454

{

t.resize(ReadInt("Hány vektort fogsz megadni? ",

"Természetes számot kérek!", Nat()));

for(int i=0; i<(int)t.size(); ++i){

t[i].SetX(ReadReal("x = "));

t[i].SetY(ReadReal("y = "));

}

}

A Sum() függvény a tömb síkvektorait összegzi.

Vector2D Sum(const vector<Vector2D> &t)

{

Vector2D s;

for(int i=0; i<(int)t.size(); ++i){

s+=t[i];

}

return s;

}

Síkvektor típus megvalósítása

A Síkvektor típust a vector2d.h-ban, a hozzáadás és skaláris szorzás

metódusait a vector2d.cpp-ben definiáljuk.

class Vector2D{

Page 455: PROGRAMOZÁSEz 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

455

private:

double x,y;

public:

Vector2D():x(0),y(0){}

Vector2D(double a, double b):x(a),y(b){}

void SetX(double r){ x = r;}

void SetY(double r){ y = r;}

Vector2D operator+=(const Vector2D &v2);

friend double operator*(const Vector2D &v1,

const Vector2D &v2);

};

Egy vektorhoz egy másikat hozzáadó metódust definiálhattuk volna a

void Add(const Vector2D& v2) metódussal is (amit v1.Add(v2)-ként

hívnánk meg), de inkább a += operátor felüldefiniálását választottuk. Ezt a

Vector2D operator+=(const Vector2D &v2) metódust ugyanis a v1

+= v2 utasítás segítségével hívhatjuk majd meg, ami sokkal

szemléletesebben fejezi ki a metódus tevékenységét.

Két síkvektor skaláris szorzását nem lenne szerencsés az osztály

metódusaként, tehát double Scalar(const Vector2D &v2)-ként

definiálni, mert ez kiemelt helyzetbe kényszerítené az egyik vektort, azt,

amelyikre, mint hívó objektumra, meg kellene majd hívni a metódust. Ekkor

ugyanis egy d = v1.Scalar(v2) hívást kellene alkalmazni, ami láthatóan

nem „szimmetrikus” a két vektorra nézve. Ha azonban a double

operator*(const Vector2D &v2) operátor felüldefiniálást használnánk,

akkor ez a d = v1*v2 utasítással hívható, ami már sokkal tetszetősebb.

Page 456: PROGRAMOZÁSEz 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

456

Hasonlóan „szimmetrikus” megoldást kínál a friend double

Scalar(const Vector2D &v1, const Vector2D &v2) alkalmazása.

Ez ugyan nem az osztály metódusa, de a friend tulajdonság miatt ugyanúgy

hivatkozhat a törzsében az osztály privát adattagjaira, mint a metódusok,

ennél fogva az osztály külső metódusának tekinthető. Nem hívó

objektummal aktiváljuk, hanem a d = Scalar(v1,v2) hívással, de a

paraméterek között szerepel legalább egy (itt kető) vektor típusú objektum.

Ugyanezt operátorként bevezetve a friend double operator*(const

Vector2D &v1, const Vector2D &v2) is biztosítja, amely a d =

v1*v2 utasítással hívható. Annak érdekében, hogy a megoldásunkban legyen

operátor felüldefiniálás és barát függvény is, ez utóbbi változatot használjuk.

Vector2D Vector2D::operator+=(const Vector2D &v)

{

x+=v.x; y+=v.y;

return *this;

}

double operator*( const Vector2D& v1,

const Vector2D &v2)

{

return v1.x*v2.x + v1.y*v2.y;

}

Tesztelés

A Vector2D komponens teszteje:

A teszteléshez célszerű az osztályban egy olyan metódust is készíteni,

amelyik meg tud jeleníteni egy síkvektort (kiírja a koordinátáit). Ezek után

készítünk egy tesztprogramot (egy menüt), amely tetszőleges sorrendben

hívhatja az osztály metódusait.

Page 457: PROGRAMOZÁSEz 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

457

1. Konstruktor teszt. (Létrejön-e a nullvektor?)

2. SetX() és SetY() tesztje. (Megváltozik egy síkvektor koordinátája?)

3. Hozzáadás műveletének tesztje. (Egy vektorhoz hozzáadni a nullvektort,

az egységvektorokat, egy tetszőleges vektort.)

4. Skalárszorzás tesztje. (nullvektorok szorzása, nem nullvektorok szorzása,

kommutatívitás)

Variációs teszt (a metódusok különféle sorrendben történő kipróbálása) itt

nem kell.

Fekete doboz tesztesetek: A Sum()-beli összegzésre és a vizsgált vektor, a

tömbbeli vektorok eredőjének merőlegességre vonatkozó tesztek, illetve a

különleges adatok (nulla, egy, negatív számok, törtek).

1. A vizsgált vektor nullvektor (0.0, 0.0) és üres a vektortömb. Válasz:

merőleges.

2. A vizsgált vektor nullvektor és nem üres vektortömb. Válasz: merőleges.

3. A vizsgált vektor nem nullvektor és a vektortömb eredője nullvektor

([(0.0, 0.0)] vagy [(3.0 ,3.0), (-3.0,-3.0)]). Válasz: merőleges.

4. A vizsgált vektor (3.0, 3.0) és a vektortömb [(-3.0, 3.0)] . Válasz:

merőleges.

5. A vizsgált vektor (3.0, 3.0) és a vektortömb [(-3.5, -3.71)] . Válasz: nem

merőleges.

6. A vektortömb első/utolsó eleme nullvektor, de a vizsgált síkvektor az

eredőre nem merőleges.

Page 458: PROGRAMOZÁSEz 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

458

Fehér doboz tesztesetek:

A fenti esetek a Sum() függvényt kielégítően tesztelik. Itt tehát csak a Fill()

függvényt kell még tesztelni: hibás adatok (pl. negatív darabszám) bevitele.

Page 459: PROGRAMOZÁSEz 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

459

Teljes program

main.cpp:

#include <iostream>

#include <string>

#include <vector>

#include "vector2D.h"

#include "read.h"

using namespace std;

void Fill(vector<Vector2D> &t);

Vector2D Sum(const vector<Vector2D> &t);

int main()

{

cout << "Összeadandó vektorok:" << endl;

vector<Vector2D> t;

Fill(t);

Vector2D s = Sum(t);

cout << "Külön vektor:" << endl;

Vector2D v( ReadReal("x = ",""),

ReadReal("y = ",""));

if(s*v == 0.0) cout << "Merőleges";

Page 460: PROGRAMOZÁSEz 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

460

else cout << "Nem merőleges.";

char ch; cin >> ch;

return 0;

}

void Fill(vector<Vector2D> &t)

{

t.resize(ReadInt("Hány vektort fogsz megadni? ",

"Természetes számot kérek!", Nat));

for(int i=0; i<(int)t.size(); ++i){

t[i].SetX(ReadReal("x = ",""));

t[i].SetY(ReadReal("y = ",""));

}

}

Vector2D Sum(const vector<Vector2D> &t)

{

Vector2D s;

for(int i=0; i<(int)t.size(); ++i){

s+=t[i];

}

return s;

}

vector2d.h:

#ifndef VECTOR2D_H

#define VECTOR2D_H

Page 461: PROGRAMOZÁSEz 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

461

class Vector2D{

private:

double x,y;

public:

Vector2D():x(0),y(0){}

Vector2D(double a, double b):x(a),y(b){}

void SetX(double r){ x = r;}

void SetY(double r){ y = r;}

Vector2D operator+=(const Vector2D &v2);

friend double operator*(const Vector2D &v1,

const Vector2D &v2);

};

#endif

vector2d.cpp:

#include "vector2D.h"

using namespace std;

Vector2D Vector2D::operator+=(const Vector2D &v)

{

x+=v.x; y+=v.y;

return *this;

}

Page 462: PROGRAMOZÁSEz 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

462

double operator*( const Vector2D& v1,

const Vector2D &v2)

{

return v1.x*v2.x + v1.y*v2.y;

}

read.h:

#ifndef READ_H

#define READ_H

#include <string>

bool ci(int k);

bool cd(double k);

bool Nat(int n);

int ReadInt(std::string msg,

std::string errmsg = "",

bool check(int) = ci);

double ReadReal(std::string msg,

std::string errmsg = "",

bool check(double) = cd);

#endif

read.cpp:

#include "read.h"

Page 463: PROGRAMOZÁSEz 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

463

#include <iostream>

using namespace std;

bool ci(int k){ return true; }

bool cd(double k){ return true; }

bool Nat(int n) { return n >= 0; }

int ReadInt(string msg, string errmsg,

bool check(int) )

{

int n;

int error = true; string tmp;

do{

cout << msg; cin >> n;

if(cin.fail() || !check(n)){

cout << errmsg << endl;

cin.clear();

}

getline(cin,tmp);

}while(error);

return n;

}

double ReadReal(string msg, string errmsg,

Page 464: PROGRAMOZÁSEz 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

464

bool check(double))

{

double a;

bool error = true; string str;

do{

cout << msg; cin >> str;

a = atof(str.c_str());

error = 0 == a && str != "0";

if(error) cout<< errmsg<< endl;

}while(error);

return a;

}

Page 465: PROGRAMOZÁSEz 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

465

C++ kislexikon

osztály class{

private:

int i;

static const int n = 3;

public:

Tipus();

Tipus(…);

~Tipus();

void method1(…);

void method2(…) const;

void method3(…){ … }

Tipus operator+=(const Tipus &v);

friend void method4(…);

};

Tipus::Tipus(){…}

Tipus::Tipus(int a):a(i){…}

Tipus::~Tipus(){ … }

void Tipus::method1(…){…}

void Tipus::method2(…) const {…}

Tipus Tipus::operator+=(const Tipus &p){…}

void method4(…){…}

Page 466: PROGRAMOZÁSEz 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

466

láthatóság private , public

konstruktor Tipus();

Tipus(…);

kezdeti

értékadás

Tipus::Tipus(int a):a(i){ … }

destruktor ~Tipus();

konstans tag static const int n = 3;

hívás t.method(…);

konstans

metódus

void method2(…) const;

inline void method3(…) { … }

operátor

túlterhelés

Tipus operator+=(const Tipus &p);

// hívása: t += t1;

barát friend void method4(…);

Page 467: PROGRAMOZÁSEz 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

467

12. Felsorolók típusainak megvalósítása

Könyvünk első kötetében számos feladat megoldását terveztük a felsorolókra

általánosított programozási tételekre történő visszavezetés segítségével. Az

ilyen megoldások implementálásában az egyedüli problémát az alkalmazott

felsoroló megvalósítása jelentheti. Néha a felsoroló típusa meglévő típusok

valamelyikével kiváltható, máskor azonban egy saját osztályt kell ehhez

definiálni.

Implementációs stratégia

A felsoroló egy olyan objektum, amely bizonyos elemek felsorolását teszi

lehetővé. A felsorolandó elemek elhelyezkedhetnek közvetlenül egy

gyűjteményben (tárolóban), és ilyenkor a felsorolónak nem kell mást tennie,

mint bejárni a tárolt elemeket. Máskor viszont a felsorolni kívánt elemek

nem állnak explicit módon rendelkezésünkre, azokat elő kell állítani, ki kell

számolni. Igaz, hogy a felsoroló használata ilyenkor is azt az illúziót kelti,

mintha az általa felsorolt elemek sorozata valóban létezne, de ilyenkor a

felsoroló a valóságtól elvonatkoztatott, absztrakt objektum.

t.First()

t.End()

feldolgozás(t.Current())

t.Next()

12-1 Felsoroló által szolgáltatott elemek feldolgozása

Függetlenül azonban attól, hogy a felsoroló konkrét elemeket jár-e be

vagy csak generálja a felsorolt elemeket, a felsoroláshoz minden esetben

ugyanaz a négy művelet szükséges. A First() művelet indítja el a felsorolást

azzal, hogy rááll a felsorolás során először érintett elemre – feltéve, hogy van

ilyen. Minden további, tehát soron következő elemre a Next() művelet

Page 468: PROGRAMOZÁSEz 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

468

segítségével tudunk ráállni. A Current() művelet a felsorolás alatt kijelölt

aktuális elemet (amire éppen „ráállt” a felsorolás) adja vissza. Az End() a

felsorolás során mindaddig hamis értéket ad, amíg van kijelölt aktuális elem,

a felsorolás végét viszont igaz visszaadott értékkel jelzi.

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

First(): rááll az első felsorolandó elemre

Next(): rááll a rákövetkezendő felsorolandó elemre

Current(): visszaadja a felsorolás aktuális elemét

End(): akkor ad igazat, ha nincs már több felsorolni kívánt elem

Page 469: PROGRAMOZÁSEz 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

469

norm lesz, az e az eredeti sorozat első elemét, az f az eggyel rövidebb

sorozatot veszi fel értékként.

A szekvenciális inputfájl read műveletével kiváltható a felsorolás

klasszikus négy művelete. A fájl elemeinek felsorolása ugyanis előre-olvasási

technikával történik (12-3. ábra), amelyből látszik, hogy a First() és a Next()

műveleteket a read művelet helyettesíti, a Current() műveletet a read által

beolvasott elem (e), az End() művelet pedig az „end of file”eseményt a read

által beolvasott státuszt (st) segítségével vizsgálja.

st,e,f : read

st = norm

feldolgozás(e)

st,e,f : read

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.

Page 470: PROGRAMOZÁSEz 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

470

A felsoroló műveletek implementálásánál figyelembe vehetjük (mert

ez könnyítést jelent), hogy a műveletek hatását nem kell minden esetre

definiálni. Például nem-definiált az, hogy a First() végrehajtása előtt (tehát a

felsorolás megkezdése előtt) illetve az End() igazra váltása után (azaz a

felsorolás befejezése után) mi a hatása a Next() és a Current() műveleteknek.

Általában nem definiált az sem, hogy mi történjen akkor, ha a First()

műveletet a felsorolás közben ismételten végrehajtjuk, vagy az End()

műveletet még az előtt használjuk, hogy a felsorolás a First() művelettel

elindult volna. A felsoroló felhasználási módja, a felsorolókra épülő

programozási tételek garantálják, hogy ezen „hiányosságok” ne okozzanak

futási hibát. Hasonlóképpen azt sem e műveletek implementálásának kell

garantálnia, hogy egy felsorolás véges lépésben biztosan befejeződjön.

Egy felsoroló osztály komponens tesztje speciális, egyszerűsített

eljárással történik, mivel a felsoroló műveleteinek egymáshoz való sorrendjét

a hívó program garantálja, ennél fogva nincs szükség a metódusok

variációinak tesztelésére, csak külön-külön az egyes metódusokéra.

Nyelvi háttér

A felsoroló típusának megvalósítását – történjen az saját osztállyal vagy

meglévő típusokkal – nem igényel az eddig használtakhoz képest újabb nyelvi

elemeket.

Egy szekvenciális inputfájl elemeinek felsorolásához és feldolgozásához

egyetlen read műveletet szoktunk használni. Ennek programozási nyelvi

változatai azonban többnyire nem adják vissza közvetlenül azt az

információt, hogy elértük-e már a felsorolás során a fájlvégét. A C-szerű

nyelvekben a sikertelen olvasás (amikor a fájl elemeit már mind felsoroltuk,

azaz a fájl kiürült) nem számít hibás műveletnek, végrajtását akár többször is

megismételhetjük. Az olvasás után le tudjuk kérdezni azt, hogy az sikerült-e

vagy sem (eof vagy fail). (Ettől lényegesen eltér a Pascal nyelv: ott előbb

kérdezzük, hogy vajon nincs-e még fájlvége, és csak pozitív válasz esetén

szabad olvasni.)

f >> e;

while( !f.fail()){

feldolgozás(e);

f >> e;

}

Page 471: PROGRAMOZÁSEz 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

471

12-4. Előre-olvasási technika egy C++ nyelvi változata

A read művelet konkrét megjelenési formája a C++ nyelvben is függ a

szekvenciális inputfájl fizikai formájától és a beolvasandó adat típusától.

Mivel gyakori, hogy szöveges állományra épített szekvenciális inputfájl

elemeit soroljuk és dolgozzuk fel, ezért érdemes átismételni (lásd 5. fejezet),

hogy ezt miképpen lehet megtenni attól függően, hogy szöveges állomány

milyen formában tartalmazza az adatokat.

Szöveges állomány tartalmának karakterenkénti olvasását figyelhetjük

meg az alábbi kódrészletekben. Ebben a beolvasott karaktereket rögtön ki is

írjuk egy másik szöveges állományba, azaz itt egy karakterenkénti másolást

látunk.

ifstream x("inp.txt");

ofstream y("out.txt");

char ch;

for(x.get(ch); !x.fail(); x.get(ch)){

y.put(ch); // lehetne y << ch is

}

vagy

ifstream x("inp.txt");

ofstream y("out.txt");

char ch;

x.unsetf(ios::skipws);

for(x >> ch; !x.fail(); x >> ch){

y << ch; // lehetne y.put(ch) is

}

Page 472: PROGRAMOZÁSEz 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

472

A gyakorlatban a fenti kódok helyett az alábbiakat szokták alkalmazni.

ifstream x("inp.txt");

ofstream y("out.txt");

char ch;

while(x.get(ch)){ y.put(ch); }

vagy

ifstream x("inp.txt");

ofstream y("out.txt");

char ch;

x.unsetf(ios::skipws);

while(x >> ch){ y << ch; }

A szöveges állománynak egynél több karakterből álló részét is be

tudjuk olvasni egy adatként, és egyetlen olvasó utasítás segítségével. Az

alábbi kóddal szöveges állományból elválasztó jelekkel szeparált egész

számokat olvasunk be, és megszámoljuk a páros számokat.

ifstream x("inp.txt");

int n, db = 0;

for(x >> n; !x.fail(); x >> n){

if(n%2 == 0) ++db;

}

vagy

ifstream x("inp.txt");

int n, db = 0;

while(x >> n){

Page 473: PROGRAMOZÁSEz 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

473

if(n%2 == 0) ++db;

}

Technikailag semmi újdonságot nem tartalmaz az előzőekhez képest a

következő kódrészlet. Ez egy szöveg szavainak átlagos szóhosszát számolja ki.

Ebben elválasztó jelekkel (szóköz, tabulátor jel, sorvége jel) határolt

sztringeket (szavakat) olvasunk.

ifstream x("inp.txt");

string str;

int db, hossz;

db = hossz = 0;

for(x >> str; !x.fail(); x >> str){

hossz += str.size();

++db;

}

int atl = hossz/db;

vagy

ifstream x("inp.txt");

string str;

int db, hossz;

db = hossz = 0;

while(x >> str){

hossz += str.size();

++db;

}

Page 474: PROGRAMOZÁSEz 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

474

int atl = hossz/db;

A szöveges állományok olvasásának harmadik lehetősége a soronkénti

olvasás. Ha egy sorban szereplő adatokat elválasztó jelekkel határolják, de

egy adaton belül nincs elválasztó jel, akkor a fenti technika ilyenkor is

alkalmazható. Az alábbi kódrészletben sorszám-név párokat olvasunk a

szöveges állományból. (Ez a program akkor is működik, ha a párok nem

soronként helyezkednek el a szöveges állományban.)

ifstream x("inp.txt");

int szam;

string nev;

for(x >> szam >> nev; !x.fail();

x >> szam >> nev){ ... }

vagy

ifstream x("inp.txt");

int szam;

string nev;

while(x >> szam >> nev){ ... }

Ha az elválasztó jelek nem szeparálják egyértelműen a beolvasott

értékeket (például egy név többtagú, azaz tartalmazhat szóközöket is), de

feltehetjük, hogy a szöveges állomány soraiban az egyes adatokat rögzített

pozíciókon helyeztük el, akkor a getline() utasítás segítségével egyszerre

egy egész sort olvashatunk be egy sztringbe, amelyből ki tudjuk hasítani a

megfelelő rész-sztringeket, hogy azokból aztán a kívánt értéket kinyerjük.

Például, ha egy szöveg minden sorában az első négy pozíción egy négyjegyű

szám, az azt követő húsz pozíción egy személy (szóközöket is tartalmazó)

neve áll, akkor az alábbi kódot használhatjuk a sorszám-név párok

felsorolására.

Page 475: PROGRAMOZÁSEz 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

475

ifstream x("inp.txt");

string sor;

for(getline(x,sor); !x.fail();getline(x,sor)){

int szam = atoi(sor.substr( 0, 4).c_str());

string nev = sor.substr( 4,20);

...

}

vagy

ifstream x("inp.txt");

string sor;

while(getline(x,sor)){

int szam = atoi(sor.substr( 0, 4).c_str());

string nev = sor.substr( 4,20);

...

}

Page 476: PROGRAMOZÁSEz 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

476

30. Feladat: Könyvtár

Egy szöveges állomány egy könyvtár adatait tartalmazza. Minden könyvről

ismerjük az azonosítóját, a szerzőjét, címét, kiadóját, kiadásának évét, ISBN

számát és azt, hogy hány példány van belőle jelenleg a könyvtárban. Egy

könyv adatai a szöveges állomány egy sorát foglalja el szigorú pozicionálási

szabályok mellett. Válogassuk ki a nulla példányszámú könyvek szerzőjét és

címét.

Specifikáció

A feladat egy kiválogatás: egy szekvenciális inputfájlból kell az adott

tulajdonságú elemeket kigyűjteni, és elhelyezni őket egy szekvenciális

outputfájlban.

A = ( x : SeqInFile(Könyv), y : SeqOutFile(Könyv2) )

Könyv = rec(azon:ℕ, szerző:String, cím:String,

kiadó:String, év:String, darab:ℕ, isbn:String)

Könyv2= rec(szerző:String, cím:String)

Ef = ( x=x' )

Uf = (

címdxszerződxy

0darabdxxdx

.,.

.'

)

Absztrakt program

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

Page 477: PROGRAMOZÁSEz 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

477

y : write(<dx.szerző,dx.cím>) SKIP

sx,dx,x : read

Implementálás

Az implementálásnál azt kell szem előtt tartani, hogy a szekvenciális inputfájl

hátterében a feladatban megadott formájú szöveges állomány áll, amelyre

definiálnunk kell az olvasás (read) műveletét. A szekvenciális outputfájl write

műveletét egy szöveges állományba történő írással kell megvalósítani.

Az implementációt kétféleképpen is elkészítjük. Először egy

egyszerűbb változatot mutatunk, ahol a read és write műveleteket csak

annyira különítjük el a kód többi részétől, hogy önálló alprogramokba

ágyazzuk őket. Másodszor egy kicsit „nagyobb feneket kerekítünk” a

megoldásnak, nevezetesen elkészítjük a feladat állapotterében szereplő két

fájl típusát definiáló osztályokat, és ezen osztályoknak lesz read illetve write

metódusa. Az első változat inkább egy procedurális szemléletű, a második

egy objektum orientált szemléletű megoldást tükröz.

Első megoldás szerkezete

Az első megoldás szerkezete nagyon egyszerű. A main.cpp néhány egyszerű

felhasználói típus (Könyv, Státusz) definiálása mellett három alprogramot

tartalmaz. A main függvény gondoskodik a szöveges állományok szekvenciális

input- és outputfájlként való megnyitásáról, tartalmazza az absztrakt

program kódját, amely hívja a másik kettő alprogramot.

12-5. ábra. Alprogramok hívási láncai

main()

Read()

Write()

Page 478: PROGRAMOZÁSEz 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

478

Első megoldás kódja

A main függvényt megelőzi néhány fontos típusdefiníció. Nem definiáljuk

külön a Könyv2 típust, mert annak mezői a Könyv típus mezőinek részét

alkotják.

struct Book{

int id;

string author;

string title;

string publisher;

string year;

int piece;

string isbn;

};

enum Status{abnorm, norm};

A main függvény megpróbálja megnyitni a bemeneti szöveges

állományt és létrehozni a kimeneti szöveges állományt. Ha mindkét

tevékenység sikerül, akkor kerül végrehajtásra az absztrakt programban

rögzített kiválogatás.

int main()

{

ifstream x("inp.txt");

if (x.fail() ) {

cerr << "Nincs input file!\n";

Page 479: PROGRAMOZÁSEz 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

479

char ch; cin>>ch; exit(1);

}

ofstream y("out.txt");

if (y.fail() ) {

cerr << "Nincs output fájl!\n";

char ch; cin>>ch; exit(1);

}

Book dx;

Status sx;

for(Read(x,dx,sx); norm==sx; Read(x,dx,sx)) {

if (0 == dx.count) {

Write(y, dx.author, dx.title);

}

}

return 0;

}

A Read() művelet a klasszikus szekvenciális inputfájlból való olvasás,

amely kihasználja, hogy a bemeneti szöveges állomány formája kötött, ezért

egy teljes sor sikeres beolvasása után a sorból, mint sztringből ki lehet vágni

az egyes részadatokat.

void Read(ifstream &x, Book &dx, Status &sx)

{

string sor;

Page 480: PROGRAMOZÁSEz 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

480

getline(x,sor,'\n');

if (!x.fail()) {

sx = norm;

dx.id = atoi(sor.substr( 0, 4).c_str());

dx.author = sor.substr( 5,14);

dx.title = sor.substr(21,19);

dx.publisher = sor.substr(42,14);

dx.year = sor.substr(58, 4);

dx.count = atoi(sor.substr(63, 3).c_str());

dx.isbn = sor.substr(67,14);

}

else sx = abnorm;

}

A Write() művelet egy új sort illeszt a kimeneti szöveges

állományhoz.

void Write(ofstream &y, const string &author, const

string &title)

{

y << setw(14) << author << ' '

<< setw(19) << title << endl;

}

Második megoldás komponens szerkezete

Page 481: PROGRAMOZÁSEz 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

481

Ebben a megvalósításban két külön osztály írja le a szekvenciális inputfájl

(Stock) és a szekvenciális outputfájl (Result) típusát. Ezeket külön

csomagokba helyezzük.

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.

12-7. ábra. Alprogramok hívási láncai

main.cpp

• main()

stock.h-stock.cpp

class Stock

• Stock()

• Read()

• ~Stock()

result.h-result.cpp

class Result

• Result()

• Write()

• ~Result()

main()

Stock()

Read()

~Stock()

Result()

Write()

~Result()

Page 482: PROGRAMOZÁSEz 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

482

Második megoldás osztályai

A Törzs típust leíró Stock osztály egy objektumát, egy törzsfájlt egy

ifstream típusú objektummal reprezentáljuk. Ezt az osztály konstruktora

nyitja meg és az inline definíciójú destruktora zárja be. Az osztály ezeken

kívül még a Read() metódust tartalmazza: ez a soron következő könyv

adatainak beolvasását végzi.

Az osztály a Book és Status típusok definíciójával együtt a stock.h

állományba tesszük.

class Stock{

public:

Stock(std::string fname);

void Read(Book &df, Status &sf);

private:

std::ifstream f;

};

A stock.cpp állományban helyeztük el a konstruktor és a Read()

metódus implementációját. A konstruktor – amennyiben nem kapja meg

bemenetként – megkérdezi a megnyitandó szöveges állomány nevét, majd

megkísérli azt megnyitni.

Stock::Stock(string fname = "")

{

if ( fname.size()<1 ) {

cout << "Add meg a törzsfájl nevét:" ;

cin >> fname;

Page 483: PROGRAMOZÁSEz 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

483

}

f.open(fname.c_str());

if ( f.fail() ){

cerr << "Nincs törzs fájl" <<endl;

char ch; cin>>ch; exit(2);

}

}

A Read() metódus törzse szóról szóra megegyezik az első változat

Read() műveletével. Csak a metódus paraméterezése tér el attól, és ennek

megfelelően a hívásának a formája.

void Stock::Read(Book &df, Status &sf)

{

string sor;

getline(f, sor,'\n');

if (!f.fail()) {

sf = norm;

df.id = atoi(sor.substr( 0, 4).c_str());

Page 484: PROGRAMOZÁSEz 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

484

df.author = sor.substr( 5,14);

df.title = sor.substr(21,19);

df.publisher = sor.substr(42,14);

df.year = sor.substr(58, 4);

df.piece = atoi(sor.substr(63, 3).c_str());

df.isbn = sor.substr(67,14);

}

else sf = abnorm;

}

Az Eredm típust leíró Result osztály a result.h állományba kerül.

class Result{

public:

Result(std::string fname);

void Write(const std::string &author,

const std::string &title);

private:

std::ofstream f;

};

Az result.cpp állományban helyeztük el a konstruktor és a Write()

metódus implementációját. A konstruktor szinte szó szerinti mása a Stock

osztály konstruktorának.

Page 485: PROGRAMOZÁSEz 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

485

Result::Result(string fname = "")

{

if ( fname.size()<1 ) {

cout << "Add meg a törzsfájl nevét:" ;

cin >> fname;

}

Page 486: PROGRAMOZÁSEz 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

486

f.open(fname.c_str());

if ( f.fail() ){

cerr << "Nincs törzs fájl" <<endl;

char ch; cin>>ch; exit(2);

}

}

A Write() metódus lényegében megegyezik az első változat Write()

műveletével.

void Result::Write(const string &author,

const string &title)

{

f << setw(14) << author << ' '

<< setw(19) << title << endl;

}

Második megoldás főprogramja

A fent bevezetett osztályoknak köszönhetően a main függvény kizárólag az

absztrakt program kódját tartalmazza, ezáltal végletesen mellőz minden, a

konkrét implementációval kapcsolatos részletet. Ezek ugyanis az

osztályokban vannak elrejtve.

int main()

Page 487: PROGRAMOZÁSEz 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

487

{

Stock x("inp.txt");

Result y("out.txt");

Book dx;

Status sx;

for(x.Read(dx,sx); norm==sx; x.Read(dx,sx)) {

if (0==dx.piece) y.Write(dx.author, dx.title);

}

return 0;

}

Tesztelés

A két változat fekete doboz tesztesetei megegyeznek: Ennek alapját a

kiválogatás (összegzés) adja, amelyiknél elsősorban az „intervallum” tesztre

(amit most a szekvenciális inputfájl vált ki) kell figyelni.

1. Üres törzsfájl esete.

2. Nem üres, csupa nulla darabszámú könyv a törzsfájlban.

3. Ne üres, csupa nem-nulla darabszámú könyv a törzsfájlban.

4. Általános eset nem üres törzsfájlra.

5. Az első és az utolsó könyv darabszáma nulla a törzsfájlban.

A fehér doboz tesztelése sem tér el egymástól a két verziónak. Az, amit

ennek keretében külön meg kell vizsgálni, az a fájlnyitásoknak, illetve az

írás/olvasásnak a megfelelő működése és hibakezelése. Ebben a tekintetben

a második változat annyival tud többet, hogy ha a fájl nevét nem adjuk meg,

Page 488: PROGRAMOZÁSEz 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

488

akkor megkérdezi azt. Az osztályoknak a modul tesztjeit nem részletezzük,

mert azokból nem adódnak újabb tesztesetek.

A második változatban elvileg komponens tesztet is kell csinálni. Tekintettel

arra, hogy az osztályoknak lényegében egy-egy metódusa van, amelyek

egyszerű beolvasást illetve kiírást végeznek, ezek vizsgálatához a korábbi

tesztesetek elegendőek. Variációs teszt nem kell.

Page 489: PROGRAMOZÁSEz 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

489

Teljes program

Az első változat teljes kódja:

#include <fstream>

#include <iostream>

#include <iomanip>

#include <string>

using namespace std;

struct Book{

int id;

string author;

string title;

string publisher;

string year;

int piece;

string isbn;

};

enum Status{abnorm, norm};

void Read(ifstream &x, Book &dx, Status &sx);

void Write(ofstream &x, const string &author,

const string &title);

Page 490: PROGRAMOZÁSEz 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

490

int main()

{

ifstream x("inp.txt");

if (x.fail() ) {

cerr << "Nincs input file!\n";

char ch; cin>>ch; return 1;

}

ofstream y("out.txt");

if (y.fail() ) {

cerr << " Nincs output fájl!\n";

char ch; cin>>ch; return 1;

}

Book dx;

Status sx;

for(Read(x,dx,sx); norm==sx; Read(x,dx,sx)) {

if (0 == dx.piece) {

Write(y, dx.author, dx.title);

}

}

return 0;

}

void Read(ifstream &x, Book &dx, Status &sx)

{

Page 491: PROGRAMOZÁSEz 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

491

string sor;

getline(x,sor,'\n');

if (!x.fail()) {

sx = norm;

dx.id = atoi(sor.substr( 0, 4).c_str());

dx.author = sor.substr( 5,14);

dx.title = sor.substr(21,19);

dx.publisher = sor.substr(42,14);

dx.year = sor.substr(58, 4);

dx.piece = atoi(sor.substr(63, 3).c_str());

dx.isbn = sor.substr(67,14);

}

else sx = abnorm;

}

void Write(ofstream &y, const string &author,

const string &title)

{

y << setw(14) << author<< ' '

<< setw(19) << title << endl;

}

Page 492: PROGRAMOZÁSEz 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

492

A második változat teljes kódja:

main.cpp:

#include <fstream>

#include <string>

#include "stock.h"

#include "result.h"

using namespace std;

int main()

{

Stock x("inp.txt");

Result y("out.txt");

Book dx;

Status sx;

for(x.Read(dx,sx); norm==sx; x.Read(dx,sx)) {

if (0 == dx.piece) y.Write(dx.author, dx.title);

}

return 0;

}

stock.h:

#ifndef _STOCK_

#define _STOCK_

#include <fstream>

Page 493: PROGRAMOZÁSEz 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

493

#include <string>

struct Book {

int id;

std::string author;

std::string title;

std::string publisher;

std::string year;

int piece;

std::string isbn;

};

enum Status {abnorm,norm};

class Stock{

public:

Stock(std::string fname);

void Read(Book &df, Status &sf);

private:

std::ifstream f;

};

#endif

stock.cpp:

#include "stock.h"

Page 494: PROGRAMOZÁSEz 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

494

#include <iostream>

#include <cstdlib>

using namespace std;

Stock::Stock(string fname = "")

{

if ( fname.size()<1 ) {

cout << "Add meg a törzsfájl nevét:" ;

cin >> fname;

}

f.open(fname.c_str());

if ( f.fail() ){

cerr << "Nincs törzs fájl" <<endl;

char ch; cin>>ch;

exit(2);

}

}

void Stock::Read(Book &df, Status &sf)

{

string sor;

getline(f, sor,'\n');

if (!f.fail()) {

Page 495: PROGRAMOZÁSEz 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

495

sf = norm;

df.id = atoi(sor.substr( 0, 4).c_str());

df.author = sor.substr( 5,14);

df.title = sor.substr(21,19);

df.publisher = sor.substr(42,14);

df.year = sor.substr(58, 4);

df.piece = atoi(sor.substr(63, 3).c_str());

df.isbn = sor.substr(67,14);

}

else sf = abnorm;

}

Page 496: PROGRAMOZÁSEz 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

496

result.h:

#ifndef _RESULT_

#define _RESULT_

#include <fstream>

#include <string>

class Result{

public:

Result(std::string fname);

void Write(const std::string &author,

const std::string &title);

private:

std::ofstream f;

};

#endif

result.cpp:

#include "result.h"

#include <iostream>

#include <cstdlib>

#include <iomanip>

using namespace std;

Page 497: PROGRAMOZÁSEz 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

497

Result::Result(string fname = "")

{

if ( fname.size()<1 ) {

cout << "Add meg a törzsfájl nevét:" ;

cin >> fname;

}

f.open(fname.c_str());

if ( f.fail() ){

cerr << "Nincs törzs fájl" <<endl;

char ch; cin>>ch;

exit(2);

}

}

void Result::Write(const string &author,

const string &title)

{

f << setw(14) << author << ' '

<< setw(19) << title << endl;

}

Page 498: PROGRAMOZÁSEz 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

498

31. Feladat: Havi átlag-hőmérséklet

Adott egy szöveges állományban egy adott időszak napi

átlaghőmérsékleteinek sorozata. Az állomány minden sora egy év-hó-nap

(eehhnn) formátumban megadott dátumot tartalmaz, amelyet egy szóköz,

majd egy hőmérsékleti érték követ. Az állomány dátum szerint növekedően

rendezett. Hány olyan egymást követő hónap-pár van, ahol a havi

átlaghőmérséklet megegyezik?

Specifikáció

A feladat egy számlálás, amelyet azonban nem az állományban megadott

dátum-hőmérséklet párok sorozata felett kell közvetlenül értelmezni, hanem

azon átlaghőmérséklet-párok sorozata felett, amelyek két szomszédos hónap

havi átlaghőmérsékleteiből állnak. Bevezetjük tehát e szám-párokat

szolgáltató absztrakt felsorolót (enor(Pár)).

A = ( t : enor(Pár), darab : ℕ )

Pár = rec(akt:ℝ, elő:ℝ)

Ef = ( t=t' )

Uf = (

előaktpáraktaktpár

taktpár

1darab

..

'

)

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(ℝ)) 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()

Page 499: PROGRAMOZÁSEz 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

499

t.Next() ~ x.Next(); tvége:=x.End()

ha x.End() akkor aktpár.előző:=aktpár.akt

aktpár.akt:=x.Current()

t.Current() ~ aktpár

t.End() ~ tvége

Az x felsoroló megvalósításához a szöveges állomány dátum-

hő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ó:ℕ, hő:ℝ)) 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:ℝ). 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ó

hóhónap

fnapnap

hőnapf:napstössz

.

),(

.,,,

hóhónap

fnapnap

f:napstdb

.

),(

,,, 1

Page 500: PROGRAMOZÁSEz 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

500

havi:=össz/db

x.Current() ~ havi

x.End() ~ xvége

Absztrakt program

A feladatot megoldása több, egymásra épülő rétegből tevődik össze.

Legfelül egy számlálás, amelyik a t felsoroló működésére épül:

darab := 0

t.First()

t.End()

t.Current().akt=t.Current().elő

darab:=darab+1 SKIP

t.Next()

A t felsoroló típusa alkotja a következő szintet, amelyhez viszont az x

felsoroló típusa szükséges, amely a harmadik szint, ahol az adatfolyam

objektumra lesz szükség, amely segítségével a szöveges állomány olvasható.

Itt valójában átugrunk egy szintet, mert nem valósítjuk meg önálló

osztályként annak a szekvenciális inputfájlnak a típusát, amellyel a szöveges

állomány sorait tudjuk kiolvasni. Ezért a szekvenciális inputfájlnak a read

művelete az x felsoroló típusának szintjén kerül majd megadásra.

A t felsoroló műveleteinek implementálása nem igényel ciklust, ezért a

művelet-implementációk algoritmusát nem részletezzük. Az x felsoroló

műveletei közül csak a x.Next() műveletnek érdemes külön felírni az

absztrakt programját. Ez egy elágazás, amelyen belül két feltétel fennállásáig

(amíg ugyanazon hónap hőmérsékleteit vizsgáljuk) tartó összegzés szerepel:

egyik összeadja a napi átlaghőmérsékleteket, a másik megszámolja, hány nap

van a hónapban. Mindkét összegzés ugyanazon a két ponton tér el egy

szekvenciális inputfájl elemeinek nevezetes felsorolására épülő összegzéstől.

Page 501: PROGRAMOZÁSEz 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

501

Egyrészt az End() műveletük már a fájlvége előtt is igaz lehet, ha hónap

végére értünk, tehát a ciklusfeltétel kiegészül az aktuális hónap figyelésével.

Másrészt nem igényelnek előre olvasást, mert a hónap legelső napját vagy az

x.First(), vagy az előző x.Next() végén már beolvastuk. A két összegzést –

lévén azonos szerkezetűek – egyetlen ciklusba vonjuk össze.

xvége := st=abnorm

st=norm

hó, össz, db:=nap.hó, 0, 0

SKIP st=norm hó=nap.hó

ö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(ℝ) 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,

Page 502: PROGRAMOZÁSEz 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

502

ezért a main.cpp állományba be kell inklúdolni a pair_enor.h állományt.

A Pair_Enor osztálynak a Month_Average_Enor (enor(ℝ)) 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.

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

{

main.cpp

• main()

pair_enor.h-pair_enor.cpp

class Pair_Enor

• First()

• Next()

• End()

• Current()

average_enor.h-average_enor.cpp

class Average_Enor

• First()

• Next()

• End()

• Current()

• Open()

• Read()

Page 503: PROGRAMOZÁSEz 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

503

Pair_Enor t("input.txt");

int count = 0;

for(t.First(); !t.End(); t.Next()){

if(t.Current().curr == t.Current().prev)

++count;

}

cout << "Azonos hőmérséklet párok száma: "

<< count;

return 0;

}

A main függvény hívja a Pair_Enor osztály metódusait. A konstruktornak a

megnyitandó szöveges állomány nevét adja át.

12-9. ábra. Alprogramok hívási láncai

Pair_Enor osztály

main()

Pair_Enor()

First()

Next()

End()

Current()

Page 504: PROGRAMOZÁSEz 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

504

A tervezésben enor(Pár)-ként definiált Pair_Enor osztály a t felsoroló

típusát határozza meg. Az osztály kódja külön fej- és forrásállományba kerül.

Az osztály definíciója előtt definiálni kell a Pair (Pár) típust.

struct Pair{

double prev;

double curr;

};

class Pair_Enor{

private:

Month_Average_Enor x;

bool end;

Pair current;

public:

Pair_Enor(const std::string &str)

{ x.Open(str); }

void First();

void Next();

Pair Current() const { return current; }

bool End() const { return end; }

};

A Pair_Enor osztály definíciója inline módon tartalmazza a

Current(), az End() és a konstruktor implementálását.

Page 505: PROGRAMOZÁSEz 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

505

A tervezésnél az osztály konstruktoráról nem esett szó. Ez egy sztringet

(szöveges állomány neve) kap bemenetként, és ezzel hívja meg a

Month_Average_Enor osztály Open() függvényét, amely a szöveges

állományt nyitja majd meg.

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.

12-11. ábra. Alprogramok hívási láncai

Pair_Enor() Open()

First()

First()

Next()

End()

Current()

Next()

Next()

End()

Current()

Page 506: PROGRAMOZÁSEz 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

506

void Pair_Enor::First() {

x.First();

if(!x.End())current.prev = x.Current();

x.Next();

end = x.End();

if(!end)current.curr = x.Current();

}

void Pair_Enor::Next() {

x.Next();

end = x.End();

if (!end) {

current.prev = current.curr;

current.curr = x.Current();

}

}

Page 507: PROGRAMOZÁSEz 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

507

Month_Average_Enor osztály

A tervezésben enor(ℝ)-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;

Page 508: PROGRAMOZÁSEz 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

508

bool end;

double avrterm;

void Read();

public:

void Open(const std::string &str);

void First() { Read(); Next(); }

void Next();

double Current() const { return avrterm; }

bool End() const { return end;}

};

Az osztály definíciója inline módon tartalmazza a destruktor, a

First(), a Current() és az End() implementálását. Ezek, csakúgy, mint a

Next() művelet kódja, megfelelnek a tervezésnél megadott

implementációnak.

void Month_Average_Enor::Next()

{

end = abnorm == st;

if(!end){

avrterm = 0.0;

int c = 0;

for(int month = day.month; norm == st &&

month == day.month; Read() ){

Page 509: PROGRAMOZÁSEz 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

509

avrterm += day.term;

++c;

}

avrterm /= c;

}

}

Az Open() bemenetként a megnyitandó szöveges állomány nevét

kapja meg, és ha ez létezik, akkor egy ifstream adatfolyamot nyit erre az

állományra, amelyet a Month_Average_Enor destruktora zárja be.

void Month_Average_Enor::Open(const string &str)

{

f.open(str.c_str());

if(f.fail()){

cout << "Inputfajl hiba!\n";

exit(1);

}

}

A Read() művelet az állomány soron következő sorát beolvasva

kinyeri abból az adott nap hónapjának számát és a napi átlaghőmérsékletet.

Ennek a műveletnek a paraméterei a Month_Average_Enor privát

adattagjai, nevezetesen a f adatcsatorna, a day adat és az st státusz.

void Month_Average_Enor::Read()

Page 510: PROGRAMOZÁSEz 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

510

{

string date;

f >> date;

if(!f.fail()){

st = norm;

day.month = atoi(date.substr(2,2).c_str());

f >> day.term;

}

else st = abnorm;

}

Tesztelés

Fekete doboz tesztesetek: (számlálás és összegzések)

1. Üres állomány.

2. Egyetlen nap adatait (egy sort) tartalmazó állomány.

3. Egyetlen hónap adatait tartalmazó állomány.

4. Több, nem azonos átlaghőmérsékletű hónap adatait tartalmazó

állomány.

5. Olyan állomány, ahol csak az első két hónap átlaghőmérséklete

azonos.

6. Olyan állomány, ahol csak az utolsó két hónap átlaghőmérséklete

azonos.

7. Általános eset, ahol több egymás utáni hónap-pár

átlaghőmérséklete is azonos.

8. Általános eset, ahol minden hónap átlaghőmérséklete azonos.

Fehér doboz tesztesetek a fentieken kívül:

Page 511: PROGRAMOZÁSEz 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

511

1. Nem létező állomány név.

Komponens tesztet nem kell külön csinálni. Egyrészt itt egy felsorolót

megvalósító osztállyal van dolgunk, amelynél a metódusok variációs tesztje

elhagyható, másrészt a metódusok egyszerű beolvasást végeznek, amelyet a

fenti tesztesetek vizsgálnak.

Page 512: PROGRAMOZÁSEz 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

512

Teljes program

main.cpp:

#include <iostream>

#include "pair_enor.h"

using namespace std;

int main()

{

Pair_Enor t("input.txt");

int count = 0;

for(t.First(); !t.End(); t.Next()){

if(t.Current().curr == t.Current().prev)

++count;

}

cout << "Azonos hőmérséklet-párok száma: "

<< count;

return 0;

}

Page 513: PROGRAMOZÁSEz 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

513

Page 514: PROGRAMOZÁSEz 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

514

pair_enor.h:

#ifndef PAIR_ENOR_H

#define PAIR_ENOR_H

#include <string>

#include "month_average_enor.h"

struct Pair{

double prev;

double curr;

};

class Pair_Enor{

private:

Month_Average_Enor x;

bool end;

Pair current;

public:

Pair_Enor(const std::string &str)

{ x.Open(str); }

void First();

void Next();

Pair Current() const { return current; }

bool End() const { return end; }

};

#endif

Page 515: PROGRAMOZÁSEz 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

515

pair_enor.cpp:

#include "pair_enor.h"

void Pair_Enor::First() {

x.First();

if(!x.End())current.prev = x.Current();

x.Next();

end = x.End();

if(!end)current.curr = x.Current();

}

void Pair_Enor::Next() {

x.Next();

end = x.End();

if (!end) {

current.prev = current.curr;

current.curr = x.Current();

}

}

Page 516: PROGRAMOZÁSEz 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

516

month_average_enor.h:

#ifndef MONTH_AVERAGE_ENOR_H

#define MONTH_AVERAGE_ENOR_H

#include <fstream>

#include <string>

enum Status { abnorm, norm };

struct Day{

int month;

double term;

};

class Month_Average_Enor{

private:

std::ifstream f;

Day day;

Status st;

bool end;

double avrterm;

void Read();

public:

void Open(const std::string &str);

void First() { Read(); Next(); }

void Next();

Page 517: PROGRAMOZÁSEz 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

517

double Current() const { return avrterm; }

bool End() const { return end;}

};

#endif

month_average_enor.cpp:

#include "month_average_enor.h"

#include <iostream>

#include <cstdlib>

using namespace std;

void Month_Average_Enor::Open(const string &str)

{

f.open(str.c_str());

if(f.fail()){

cout << "Inputfájl hiba!\n";

exit(1);

}

}

Page 518: PROGRAMOZÁSEz 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

518

void Month_Average_Enor::Next()

{

end = abnorm == st;

if(!end){

avrterm = 0.0;

int db = 0;

for(int month = day.month; norm == st &&

month == day.month; Read() ){

avrterm += day.term;

++db;

}

avrterm /= db;

}

}

void Month_Average_Enor::Read()

{

string date;

f >> date;

if(!f.fail()){

st = norm;

day.month = atoi(date.substr(2,2).c_str());

f >> day.term;

}

Page 519: PROGRAMOZÁSEz 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

519

else st = abnorm;

}

Page 520: PROGRAMOZÁSEz 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

520

32. Feladat: Bekezdések

Egy szöveges állományban bekezdésekre tördelt szöveg található. Egy

bekezdés egy vagy több nem üres sorból áll. A bekezdéseket üres sorok vagy

az állomány eleje illetve vége határolja.

Melyik a leggazdagabb bekezdés, azaz hányadik az a legalább három soros

bekezdés, amelyik tartalmazza az „alma” szót önmagában vagy valamilyen

szóösszetételben és az ilyen bekezdések közül nála a legnagyobb a szavak

számának és a sorok számának hányadosa? A szövegben egyik szó sincs több

sorra tördelve, a szavakat szóközök, tabulátor-jelek és sorvége-jelek (akár

egymás után több is) választhatja el egymástól.

Specifikáció

A feladat megoldása egy feltételes maximumkeresés, amelyik azt a bekezdést

keresi meg az „alma” szót tartalmazó bekezdések között, ahol a legnagyobb a

szavak számának és a sorok számának hányadosa. Ehhez egy olyan absztrakt

felsorolóra van szükségünk, amely az egyes bekezdések statisztikáit képes

megadni: a bekezdés sorszámát, sorainak számát, szavainak számát,

valamint, hogy szerepel-e benne az „alma” szó.

A = ( t : enor(Bekezdés), l : 𝕃 , max : ℝ, ind : ℕ )

Bekezdés = rec(sorsz:ℕ, szó:ℕ, sor:ℕ, alma:𝕃)

Ef = ( t=t' )

Uf = ( soreszóeelemmaxl

3sorealmaet'e

./.,,

..

max

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

Page 521: PROGRAMOZÁSEz 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

521

beolvasni. El kell készítenünk egy read műveletet, amely beállítja az olvasás

státuszát és a beolvasott sort egy sztring típusú változóba helyezi (st:Státusz,

sor:Sztring). Ezen kívül tárolnunk kell a legutoljára beolvasott bekezdés

statisztikáját (akt:Bekezdés), és azt a logikai értéket (vége:𝕃), amely jelzi, ha

már nincs több bekezdés. A t felsoroló műveleteit az alábbiak szerint

implementáljuk:

t.First() ~ akt.sorsz:=0

st, sor, f:read

t.Next()

t.Current() ~ akt

t.End() ~ vége

t.Next() ~ )(,,),(

sorüresfsorstfsorsor

select

vége:= st=abnorm

ha st=norm akkor

akt.sorsz:= akt.sorsz+1

soralmaf:sorstalmaaktsorüres

fsorsor

"",,,.

)(

),(

)(

),(

,,,.

sorüres

fsorsor

f:sorstsorakt 1

)(

),(

,,,.

sorüres

fsorsor

számaszavaksorbelif:sorstszóakt

Page 522: PROGRAMOZÁSEz 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

522

Absztrakt program

A feladatot megoldó feltételes maximumkeresés:

l:= hamis; t.First()

t.End()

bek:=t.Current()

(bek.alma

bek.sor≥3 )

bek.alma bek.sor≥3 l bek.alma bek.sor≥3

l

SKIP bek.szó/bek.sor>max l, max, ind :=

igaz, bek.szó/bek.sor,

bek.sorsz max, ind:=

bek.szó/bek.sor,

bek.sorsz

SKIP

t.Next()

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

SKIP

st=norm üres(sor)

akt.alma := akt.alma „alma”sor

akt.sor:=akt.sor+1

akt.szó := akt.szó+

Page 523: PROGRAMOZÁSEz 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

523

(sor-beli szavak száma)

st, sor, f : read

Az akt.alma, akt.sor és akt.szó értékét meghatározó összegzések

ciklusait egy ciklusba vonjuk össze. Az üres(sor), „alma”sor és a sor-beli

szavak száma kifejezéseket majd az implementációban pontosítjuk.

Implementálás

A t felsoroló típusát egy osztály segítségével célszerű megadni. Az

osztályok privát tagjait a felsorolót reprezentáló elemek alkotják, valamint a

szöveges állomány egy sorát kiolvasó read művelet.

Ezen a túlmenően az implementálásra már csak annyi maradt, hogy

megtaláljuk a helyes kódot az olyan részletek leírására, mint az üres(sor),

„alma”sor és a sor-beli szavak száma.

A program komponens szerkezete

A feltételes maximumkeresést a main.cpp forrásállomány main

függvényben helyezzük el a kiírással együtt, a felsoroló típusát leíró osztályt

pedig külön fej- és forrásállományban (enor.h, enor.cpp) adjuk meg. A

main.cpp állománynak be kell inklúdolnia az enor.h állományt.

A program komponens szerkezete meglehetősen egyszerű.

Page 524: PROGRAMOZÁSEz 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

524

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

main.cpp

• main()

enor.h-enor.cpp

class Enor

• Enor()

• First()

• Next()

• End()

• Current()

• ~Enor()

Page 525: PROGRAMOZÁSEz 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

525

max = rate;

ind = bek.no;

} else {

l = true;

max = rate;

ind = bek.no;

}

}

if (l) cout << "A \"leggazdagabb\" bekezdés a "

<< ind << "-dik\n"

<< "arány: " << max << endl;

else cout << "Nincs \"gazdag\" bekezdés!\n";

return 0;

}

A main függvény hívja az Enor osztály metódusait. A konstruktornak a

megnyitandó szöveges állomány nevét adja át. A függvény befejeződésekor a

destruktor is meghívódik.

Page 526: PROGRAMOZÁSEz 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

526

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;

};

main()

Enor()

First()

Next()

End()

Current

Current()

Page 527: PROGRAMOZÁSEz 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

527

Az Enor osztály privát tagjai között találjuk a felsorolót reprezentáló

ifstream típusú f objektumot (szekvenciális inputfájl), az aktuális bekezdés

statisztikáját tartalmazó current változót (tervezésnél akt néven szerepelt),

és a felsorolás végét jelző end flag-et (vége). Ezek kiegészülnek a szöveges

állományból való olvasás adataival: az aktuális sort tartalmazó line nevű

istringstream taggal (sor) és az utolsó olvasás státuszával (st). Privát

elem lesz a Read() olvasó metódus is.

class Enor{

private:

std::ifstream f;

std::istringstream line;

Status st;

bool end;

Statistic current;

void Read();

Az osztály definíciója inline módon tartalmazza a destruktor, a

First(), a Current() és az End() implementálását. Ezek, csakúgy, mint a

Next() művelet kódja, megfelelnek a tervezésnél megadott

implementációnak.

public:

Enor(const std::string &str);

void First()

{ current.no = 0; Read(); Next();}

void Next();

Page 528: PROGRAMOZÁSEz 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

528

Statistic Current() const { return current;}

bool End() const { return end;}

};

A Next() metódus megvalósításánál meg kell vizsgálni, hogy a C++

nyelv milyen lehetőségeket kínál az üres(sor), az „alma”sor és a sor-beli

szavak száma kifejezések implementálására.

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;

Page 529: PROGRAMOZÁSEz 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

529

for(;norm==st && line.str().size() != 0;

Read()){

++current.line;

string w;

for(line>>w; !line.fail(); line>>w){

++current.word;

current.apple = current.apple ||

w.find("alma")!=string::npos;

}

}

}

}

Az Enor konstruktora nyit egy ifstream csatornát a paraméterként

megadott nevű szöveges állományra.

void Enor::Enor(const string &str)

{

f.open(str.c_str());

if(f.fail()){

cout << "Inputfájl hiba!\n";

exit(1);

}

}

Page 530: PROGRAMOZÁSEz 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

530

A Read() művelet az állomány soron következő sorát olvassa be, mint

egy sztringet, és ezt alakítja át istringstream típusú adattá. Erre szolgál a

line.clear() és a line.str() metódus.

void Enor::Read()

{

string str;

getline(f,str,'\n');

if(!f.fail()){

st = norm;

line.clear();

line.str(str);

}

else st = abnorm;

}

Tesztelés

Fekete doboz tesztesetek: (Feltételes maximum keresés, azon belül

kiválasztás és összegzések.)

1. Üres állomány

2. Egyetlen legalább három soros bekezdés, ahol nincs „alma” szó.

3. Egyetlen egy soros bekezdés, amely tartalmaz „alma” szót.

4. Egyetlen két soros bekezdés (előtte és utána több üres sorral),

amelynek minden sora tartalmaz „alma” szót.

Page 531: PROGRAMOZÁSEz 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

531

5. Egyetlen legalább három soros bekezdés (előtte és utána több üres

sorral), amelynek az első sorának első szava „alma”.

6. Egyetlen legalább három soros bekezdés (előtte és utána több üres

sorral), amelynek az első sorának utolsó szava „alma”.

7. Egyetlen legalább három soros bekezdés (előtte és utána több üres

sorral), amely utolsó sorának első szava „alma”.

8. Egyetlen legalább három soros bekezdés (előtte és utána több üres

sorral), amely utolsó sorának utolsó szava „alma”.

9. Egyetlen legalább három soros bekezdés (előtte és utána több üres

sorral), amely középső sorának első szava „alma”.

10. Egyetlen legalább három soros bekezdés (előtte és utána több üres

sorral), amely középső sorának utolsó szava „alma”.

11. Több legalább három soros, „alma” szavas bekezdés, a bekezdések

előtt, között, utána több üres sorral, és a legelsőben a legnagyobb a

szó/sor arány.

12. Több legalább három soros, „alma” szavas bekezdés, a bekezdések

előtt, között, utána több üres sorral, és a legutolsóban a legnagyobb

a szó/sor arány.

13. Több legalább három soros, „alma” szavas bekezdés, a bekezdések

előtt, között, utána több üres sorral, és egy középsőben a

legnagyobb a szó/sor arány.

14. Több legalább három soros, „alma” szavas bekezdés, a bekezdések

előtt, között, utána több üres sorral, és minden bekezdés egyformán

gazdag.

15. Általános eset több bekezdéssel, a bekezdések előtt, között, utána

több üres sorral.

Fehér doboz tesztesetek a fentieken kívül:

1. Nem létező állomány név.

Komponens tesztet nem kell külön csinálni. Egyrészt itt egy felsorolót

megvalósító osztállyal van dolgunk, amelynél a metódusok variációs tesztje

elhagyható, másrészt a metódusok a Next() kivételével egyszerű beolvasást

Page 532: PROGRAMOZÁSEz 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

532

végeznek, amelyet a fenti tesztesetek vizsgálnak. A Next() metódus kódja

programozási tételre támaszkodik, amelyet a fekete doboz tesztesetek

érintettek.

Page 533: PROGRAMOZÁSEz 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

533

Teljes program

main.cpp:

#include <iostream>

#include "enor.h"

using namespace std;

int main()

{

Enor t("input.txt");

int ind;

double max;

bool l = false;

for(t.First(); !t.End(); t.Next()){

Statistic bek = t.Current();

if(!(bek.apple && bek.line>=3)) continue;

double rate

= (double)bek.word/(double)bek.line;

if(l && rate>max) {

max = rate;

ind = bek.no;

}

else {

l = true;

Page 534: PROGRAMOZÁSEz 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

534

max = rate;

ind = bek.no;

}

}

if (l) cout << "A \"leggazdagabb\" bekezdés a "

<< ind << "-dik\n"

<< "arány: " << max << endl;

else cout << "Nincs \"gazdag\" bekezdés!\n";

return 0;

}

Page 535: PROGRAMOZÁSEz 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

535

enor.h:

#ifndef ENOR_H

#define ENOR_H

#include <fstream>

#include <string>

#include <sstream>

enum Status { abnorm, norm };

struct Statistic{

bool apple;

int word;

int line;

int no;

};

class Enor{

private:

std::ifstream f;

std::stringstream line;

Status st;

bool end;

Statistic current;

void Read();

Page 536: PROGRAMOZÁSEz 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

536

public:

Enor(const std::string &str);

void First()

{ current.no = 0; Read(); Next();}

void Next();

Statistic Current() const { return current;}

bool End() const { return end;}

};

#endif

Page 537: PROGRAMOZÁSEz 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

537

enor.cpp:

#include "enor.h"

#include <iostream>

#include <cstdlib>

using namespace std;

Enor::Enor(const string &str)

{

f.open(str.c_str());

if(f.fail()){

cout << "Inputfájl hiba!\n";

exit(1);

}

}

void Enor::Next()

{

for(;norm==st && line.str().size()==0; Read());

end = abnorm == st;

if(!end){

++current.no;

current.word = current.line = 0;

current.apple = false;

for(;norm==st && line.str().size() != 0;

Page 538: PROGRAMOZÁSEz 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

538

Read()){

++current.line;

string w;

for(line>>w; !line.fail(); line>>w){

++current.word;

current.apple = current.apple ||

w.find("alma")!=string::npos;

}

}

}

}

void Enor::Read()

{

string str;

getline(f,str,'\n');

if(!f.fail()){

st = norm;

line.clear();

line.str(str);

}

else st = abnorm;

}

Page 539: PROGRAMOZÁSEz 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

539

C++ kislexikon

szöveges

állomány

karakterenkénti

olvasása

ifstream x("inp.txt");

char ch;

for(x.get(ch); !x.fail(); x.get(ch)){

...

}

ifstream x("inp.txt");

char ch;

x.unsetf(ios::skipws);

for(x >> ch; !x.fail(); x >> ch){

...

}

szöveges

állomány

számainak

olvasása

ifstream x("inp.txt");

int n, db = 0;

for(x >> n; !x.fail(); x >> n){

...

}

szöveges

állományból

szám-név párok

olvasása

ifstream x("inp.txt");

int szam;

string nev;

for(x>>szam>>nev;

!x.fail();

x>>szam >>nev){

Page 540: PROGRAMOZÁSEz 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

540

...

}

szöveges

állomány

soronkénti

olvasása, a

sorokból szám

és név

kinyerése

ifstream x("inp.txt");

string sor;

for( getline(x,sor,'\n');

!x.fail();

getline(x,sor,'\n')){

int szam =

atoi(sor.substr( 0, 4).c_str());

string nev = sor.substr( 4,20);

...

}

Page 541: PROGRAMOZÁSEz 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

541

13. Dinamikus szerkezetű típusok osztályai

Egy típus adatszerkezetén a típus egy értékét reprezentáló elemek

egymáshoz való viszonyát, rákövetkezési kapcsolatainak rendszerét értjük.

Sok típusnak statikus (állandó) adatszerkezete van, azaz a típusértékek

mindegyikét azonos számú elem helyettesíti és az elemek közötti kapcsolatok

is rögzítettek. Ennél fogva két típusérték csak az elemeik értékeiben

különbözhetnek. Tipikusan ilyen a tömb vagy a rekord típus. Amikor azonban

egy típusértéket reprezentáló elemek száma és/vagy azok kapcsolati

rendszere változhat, akkor dinamikus (változó) adatszerkezetről

beszélhetünk. Gondoljunk például egy karakterlánc típusra, amelynek értékei

különböző hosszú (eltérő számú karaktert tartalmazó) sorozatok, és ahol

megengedett egy ilyen sorozat egy részének kivágása vagy az abba való

betoldás, ami megváltoztatja a karakterláncon belül a karakterek közötti

rákövetkezési kapcsolatokat.

Amikor egy összetett szerkezetű típusnak egy adott programozási

nyelven való megvalósítására kerül sor, akkor döntenünk kell többek között

arról, hogy egy típusértéket hogyan helyezzünk el, hogyan reprezentáljunk a

memóriában. Ennek egyik módja az, amikor a típusértéket alkotó elemek

számára egyszerre és egyben lefoglaljuk a szükséges memóriaterületet,

amely aztán már nem változik, állandó marad. Ez a statikus reprezentáció.

Mivel ilyenkor a memóriafoglalásra tömbszerűen, azaz közvetlenül egymás

után (szekvenciálisan) kerül sor, ehhez tömböt vagy rekordot szoktunk

használni. A lefoglalt területen belül egy-egy adatelem elérése annak

pozíciója alapján történik, és ezt a pozíciót többnyire valamilyen aritmetikai

képlet alapján közvetlenül ki lehet számolni. Emiatt szokták ezt a

reprezentációs technikát szekvenciális vagy aritmetikai reprezentációnak is

hívni.

A másik lehetőség egy összetett érték tárolására az, hogy az egyes

adatelemek memóriafoglalását egyenként, különböző helyen (gyakran

különböző időpontokban) végezzük el, de minden helyfoglalásnál külön

eltároljuk azt is, hogy mely címen találhatóak az éppen lefoglalt elemmel

közvetlen kapcsolatban álló részek. Az így szétszórt memóriafoglalásokat

tehát a címeik segítségével tudjuk összefogni, azaz az adatelemeket „össze

Page 542: PROGRAMOZÁSEz 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

542

tudjuk láncolni”. Ezzel a technikával egészen bonyolult adatszerkezeteket

lehet nagyon rugalmasan megvalósítani, hiszen így egy folyamatosan változó,

dinamikus reprezentációhoz jutunk, amelyet sokszor szétszórt vagy láncolt

reprezentációnak is neveznek.

Némi zavart okozhat a fenti elnevezésekben az, hogy két különböző

fogalmi szinten is használjuk a statikus illetve dinamikus jelzőket: külön a

típus szerkezetére és külön annak megvalósítási módjára. A két szint között

erős kapcsolat van, de ugyanakkor nem törvényszerű az, hogy egy dinamikus

adatszerkezetű típus konkrét megvalósításához dinamikus reprezentációt,

egy statikus szerkezethez pedig statikus reprezentációt lehetne csak

használni.

Tovább bonyolítja a terminológiát, hogy létezik egy harmadik fogalmi

szint is, nevezetesen az, hogy a programozási nyelveknél a változók

memóriafoglalási módjának meghatározására (és ebből következően a

változó élettartamára) is ugyanezeket a jelzőket használjuk. Egy statikus

memóriafoglalású változó rögtön a program elindulásakor helyet foglal a

memóriában és végig ott marad, a dinamikus memóriafoglalásnál pedig

speciális utasítások segítségével történik a foglalás és a felszabadítás. (Ezeken

kívül az automatikus memóriafoglalást ismerjük még.) Fontos megemlíteni,

hogy a dinamikus memóriafoglalás nyelvi elemeit nem csak a dinamikus

reprezentáció megvalósításához lehet felhasználni.

A három fogalmi szint dinamikus és statikus jelzőinek lehetséges

társításai közül a legtöbb újdonságot kétség kívül a dinamikus szerkezetű

típusok dinamikus reprezentációjának dinamikus nyelvi elemek

felhasználásával történő megvalósítása ígéri. Ezért erről lesz ebben a

fejezetben.

Implementációs stratégia

Fontos implementációs kérdés, hogy egy összetett adatszerkezetű típust

statikusan vagy dinamikusan reprezentáljunk-e. Mivel egy típus viselkedése a

műveleteinek hatásától függ, könnyen előfordul, hogy két azonos viselkedésű

(azonos típusspecifikációjú) típus közül az egyik statikus, a másik dinamikus

reprezentációjú, ezek kölcsönösen megvalósítják egymást, és nekünk ki kell

Page 543: PROGRAMOZÁSEz 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

543

választani az egyiket. Hogy éppen melyiket, azt mindig a konkrét feladat

megkötései határozzák meg.

Például egy változó hosszúságú (tehát dinamikus szerkezetű) sorozat

típusára adhatunk statikus, azaz szekvenciális reprezentációt úgy, hogy

lefoglalunk egy kellő hosszúságú tömböt a sorozat elemeinek számára, és

külön tároljuk, hogy a tömb első hány darab eleme reprezentálja az éppen

aktuális sorozatot. Ennél azonban implementációs korlátot jelent az, hogy az

ábrázolható sorozatok hosszai nem léphetik át a lefoglalt tömb méretét. Ha

ilyen korlátozás bevezetését nem engedi meg a megoldandó feladat, akkor

nem ezt, hanem egy dinamikus reprezentációt kell alkalmaznunk. Ez lehet

például az előző tömbös megvalósításnak egy olyan változata, amelyben ha a

tömb mérete kicsinek bizonyul, akkor lefoglalunk egy nagyobbat, ahová majd

átmásoljuk az eddigi elemeket, de természetesen ez némi időveszteséggel

jár. Megoldható azonban a sorozat dinamikus ábrázolása egy láncolt listával

is, amely szétszórva egyenként tárolja a sorozat elemeit, és minden elem

mellett tárolja a soron következő elem címét is. Ehhez a lánchoz bármikor

lehet újabb elemet hozzáfűzni anélkül, hogy a meglévő összes elemet át

kellene mozgatni, viszont sokkal tovább tart mondjuk a huszadik elem

kiolvasása, mint az előző tömbös megoldásokban. Ez abban az esetben lesz

különösen hátrányos, ha a konkrét feladatban sokszor kell a huszadik elemre

hivatkozni, miközben a sorozat hossza csak ritkán nő meg.

Egy statikus reprezentáció általában több implementációs korlátozást

vezet be, mint a dinamikus, viszont az adatszerkezet egy elemének elérése

többnyire sokkal gyorsabb. Tömbszerűen, közvetlenül egymás után

elhelyezett elemek a tömbbeli pozíciójuk alapján ugyanis konstans futási idő

alatt elérhetők. Ezzel szemben a dinamikus reprezentáció, különösen a

láncolt reprezentáció egy sokkal rugalmasabb adatábrázolást tesz lehetővé,

de az egyes elemek elérése általában tovább tart, mert csak az elemek

közötti kapcsolatok mentén végig haladva lehet egy adott elemhez eljutni.

Bárhogyan is valósítunk meg egy összetett szerkezetű adatot, az

minden esetben gyűjteményként (tárolóként) viselkedik, ezért különösen

érdekes az, hogyan lehet az elemi értékeit felsoroltatni, bejárni. Ez a bejárás

a dinamikusan megvalósított összetett szerkezetű típusoknál nem egyszerű,

de ha meg tudjuk valósítani a felsorolót, akkor az elemeik feldolgozására

Page 544: PROGRAMOZÁSEz 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

544

könnyedén alkalmazhatjuk a felsorolókra általánosított programozási

tételeket is. (lásd első kötet)

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ó

h

Egyirányú lista fejelem nélkül

nil

h

Egyirányú lista fejelemmel

nil

fejelem

Page 545: PROGRAMOZÁSEz 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

545

listaelemeknek is legalább két címrésze van: az egyik a baloldali, a másik a

jobboldali gyerekcsúcsot leíró listaelem címe.

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

nil nil

nil nil nil nil nil nil

Page 546: PROGRAMOZÁSEz 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

546

(garbage collector) bízzák. A továbbiakban a C++ nyelvi lehetőségeit

tárgyaljuk.

A pointerváltozó egy közönséges változó, amely egy olyan

memóriaterület címét veheti fel értékül, ahol a pointerváltozó típusának

megfelelő értéket lehet tárolni. A pointerváltozóban tárolt memóriacím

számára automatikusan jön létre helyfoglalás, de azt a területet, amelyre ez

a cím mutat (amelynek címét majd a pointerváltozó őrzi) a programozónak

külön utasítással (new) kell lefoglalnia a dinamikus memóriából (heap), és ha

már nincs rá szükség, felszabadítania (delete). A pointerváltozó segítségével

közvetett módon tudunk hivatkozni egy általunk lefoglalt memóriaterületen

tárolt értékre, amelyet egy név nélküli változó értékének tekinthetünk.

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

cím

memória pointerváltozó

regisztráció

érték

név típus cím

Page 547: PROGRAMOZÁSEz 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

547

helyfoglalását. Maga a p változó ettől még nem szűnik meg, de helyfoglalás

hiányában a *p hivatkozás ezután már illegális, futás közben hibához vezet.

Megtehetjük azt is, hogy egy int i változó címét (ennek jele: &i) egy

int *p pointerváltozóban tároljuk el (p = &i), és ezt követően az i változó

tartalmára *p alakban is tudunk majd hivatkozni.

Egy int *p pointerváltozó arra is alkalmas, hogy egy konstans vagy

futásközben automatikusan lefoglalt tömb (int t[n]) elemeire hivatkozni

lehessen a segítségével. A p=t értékadás után a p pointer a tömb első

(nulladik indexű) elemére mutat majd, azaz a *p és a t[0] ugyanazon

memóriacímen található értéket jelenti. Ezekkel egyenértékű a p[0] és *t is.

A p+2 kifejezés a tömb harmadik (2. indexű) elemére mutat (feltételezve,

hogy van legalább három eleme a tömbnek), mert azt a memóriacímet adja

meg, amelyik a p-ben tárolt címhez képest kétszer annyi byte-tal mutat

hátrébb, ahány egy integer tárolásához kell. Így a *(p+2) ugyanarra a

tömbelemre hivatkozik, mint a t[2] (vagy a *(t+2) vagy a p[2]). (Ha a

tömbnek nem foglaltunk volna le legalább három elemet, a p+2 cím akkor is

értelmes, megnézhetjük, mi található ott, és a *(p+2) visszaadja az ezen a

címen kezdődő int-hez szükséges számú bájtból kiszámolható egész számot,

sőt ez a szám meg is változtatható. Természetesen ez súlyos futási hibákat

okozhat.) A fentiekből kikövetkeztethető az is, hogy egy tömbváltozó éppen a

tömb első (nulladik indexű) elemének címét tartalmazza, azaz t == &t[0].

Egy automatikus helyfoglalású tömb a verem memóriában (stack)

foglal helyet, ezért nem alkalmas arra, hogy egy alprogramban hozzuk létre

és onnan adjuk vissza a hívás helyére, hiszen az alprogram befejeződésekor

törlődik. Ha viszont a helyfoglalást a dinamikus memóriában végezzük, akkor

az mindaddig ott marad, amíg a program be nem fejeződik, vagy fel nem

szabadítjuk.

Egy dinamikus helyfoglalású tömb készítésekor a programozónak

először egy pointerváltozót kell definiálnia, majd külön utasítással (new) kell

a dinamikus memóriában a tömb elemeinek helyet foglalni és e helyfoglalás

kezdőcímét a pointerváltozónak értékül adni.

Egy dinamikus helyfoglalású tömbváltozó tehát egy pointer, amely a

lefoglalt tömbterületre, pontosabban annak legelső elemére mutat (legelső

bájtjának címét tartalmazza). Ezt a pointerváltozót (ahogy minden

Page 548: PROGRAMOZÁSEz 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

548

pointerváltozót) tömbként használhatjuk, azaz utána írva az indexelő

operátort hivatkozhatunk a pointerváltozóban megadott cím után

elhelyezkedő valahányadik elemre.

int n;

cin >> n;

int* v = new int[n];

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

verem memória (STACK)

0 1 2 3 cím

v int* cím

dinamikus memória (HEAP)

Page 549: PROGRAMOZÁSEz 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

549

Többdimenziós tömbök, például egy mátrix esetén az automatikus

helyfoglalással az elemek sorfolytonosan egymás után kerülnek elhelyezésre

a memóriában. A dinamikus helyfoglalás esetén ez nem egészen van így. A

folyamat egyrészt két lépcsőben történik, másrészt a mátrix egyes sorai

külön-külön kerülnek elhelyezésre a dinamikus memóriában. Ezek egyben

tartásához először le kell foglalni külön egy egydimenziós tömböt a sorok

memóriacímeinek tárolására – ez tehát egy pointertömb lesz –, majd külön-

kü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.

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

cím

verem memória (STACK)

v int** cím

dinamikus memória (HEAP)

0 1 2 3

10 11 12 13

20 21 22 23

cím cím cím

Page 550: PROGRAMOZÁSEz 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

550

a w[i] a mátrix i-edik sorát, mint egydimenziós dinamikusan lefoglalt

tömböt azonosítja.

Súlyos hiba, és még futási időben is rejtve maradhat egy ideig, ha egy

dinamikus helyfoglalású tömbnek olyankor hivatkozunk az elemeire, amikor

azok még nincsenek lefoglalva vagy már fel lettek szabadítva.

A dinamikus helyfoglalású tömb felszabadításáról a programozónak

kell gondoskodni. Kétdimenziós tömbök esetén az elemek felszabadítása is

két lépcsőben történik, csak a lefoglalással ellentétes sorrendben.

for(int i=0; i<3; ++i) delete[] w[i];

delete[] w;

A fentiek mintájára kettőnél több dimenziós dinamikus helyfoglalású

tömbök is készíthetők.

Egy egyirányú láncolt lista listaelemeinek típusát (ami egy rekord) C++

nyelven a struct segítségével írhatjuk le. Az alábbi példában a listaelem

tartalma egész szám lesz, mutató része pedig egy listaelemre mutató pointer.

A struct – az osztályokhoz hasonlóan – konstruktorral is rendelkezhet. (A

struct tagjai – szemben az osztályokkal – hivatalból publikusak.) A NULL a

sehová sem mutató nil pointerértéket reprezentálja.

struct Node {

int value;

Node *next;

Node(int i=0, Node *q=NULL):

value(i), next(q){}

};

Az alábbi három utasítás a konstruktor paraméterváltozóinak

alapértelmezése miatt egyenértékű: olyan listaelemet hoznak létre, amely a

0 egész számot és a nil címet tartalmazza, magának a listaelemnek címe

pedig a p pointerváltozóba kerül.

Page 551: PROGRAMOZÁSEz 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

551

Node *p = new Node();

Node *p = new Node(0);

Node *p = new Node(0, NULL);

A láncolt listákon végezhető egyik alapművelet egy új listaelem

beszúrása illetve. A beszúrás az új elem létrehozásából, értékének

kitöltéséből és a láncba történő befűzéséből áll. Egy fejelemes lista esetén

mindig egy már létező listaelem mögé tudjuk beszúrni az listaelemet, ezért

ha mondjuk egy u pointerváltozó erre a már létező listaelemre mutat (annak

címét tartalmazza), akkor az alábbi kód végzi a 23 értéket tartalmazó

listaelem beszúrását:

Node *p = new Node(23,u->next);

u->next = p;

Fejelem nélküli lista esetében előfordulhat, hogy a legelső listaelem

elé kell egy új listaelemet beszúrni. Ezt az előzőtől némileg eltérő kód végzi

el, ahol feltesszük, hogy a legelső listaelem címét a h pointer őrzi:

h = new Node(23,h->next);

A másik alapművelet egy listaelemnek a törlése egy láncolt listából.

Ehhez először ki kell fűzni ezt az elemet, majd törölhetjük a dinamikus

memóriából. Egy listaelemet a delete p utasítás törli, ha a p pointer a

listaelemre mutat. Tegyük fel, hogy az u pointerváltozó mutatja azt a

listaelemet, amely utáni listaelemet – ha van olyan egyáltalán – kell

kitörölnünk:

Node *p = u->next;

if(p != NULL){

u->next = p->next;

delete p;

Page 552: PROGRAMOZÁSEz 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

552

}

Fejelem nélküli láncolt lista esetében szóba jöhet a legelső listaelem

törlése is, amelyre a h pointerváltozó mutat:

if(h != NULL){

Node *p = h;

h = h->next;

delete p;

}

A fentiek alapján már könnyen felépíthető illetve lebontható egy teljes

láncolt lista.

Az alábbi kód egy n elemű láncolt lista felépítését mutatja, ahol az

elemek rendre az 1-től n-ig terjedő egész számokat kapják meg értékül. A

lista elejére a h pointer mutat majd, az u és a p segédpointerek.

Fejelemmel Fejelem nélkül

Node *h = new Node(); Node *h = new Node(1);

Node *u = h; Node *u = h;

for(int i=1;i<n;i++){ for(int i=2;i<n;i++){

Node *p = new Node(i); Node *p = new Node(i);

u->next=p; u->next=p;

u = p; u = p;

} }

Page 553: PROGRAMOZÁSEz 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

553

A láncolt lista lebontása egyformán történik a fejelemes és fejelem

nélküli változatokra. Ha a h pointer a lista legelső elemére mutat, azaz annak

címét tartalmazza, akkor

while(h != NULL){

Node *p = h;

h = p->next;

delete p;

}

Fontos tevékenység egy láncolt lista bejárása. Ilyenkor a felsoroló

objektum egy listaelem címét tartalmazó pointerváltozó, amely a bejárás

során mindig az aktuális listaelemre mutat, így annak értéke kiolvasható

(Current()). Kezdetben a lista első elemére állítjuk a felsorolót (First()), a

rákövetkező listaelem címét az aktuális listaelemből olvashatjuk ki (Next()), a

felsorolás végét pedig az jelzi, ha a felsoroló a nil értéket veszi fel (End()),

amelyet lista legutolsó listaeleme tárol következő címként. Az így

megvalósított bejárás segítségével a tanult programozási tételek könnyen

alkalmazhatóak a láncolt listában tárolt értékek feldolgozására. A fejelemes

illetve fejelem nélküli esetek csak a First() művelet megvalósításában térnek

el.

Fejelemmel

First() ~ p = h->next

End() ~ NULL == p

Current() ~ p->value

Next() ~ p = p->next

Fejelem nélkül

~ p = h

~ NULL == p

~ p->value

~ p = p->next

13-6. ábra. Egyirányú lista felsoroló műveletei

Page 554: PROGRAMOZÁSEz 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

554

A 11. fejezetben bevezettük az egyszerű osztály fogalmát, amelyeket

különösebb óvintézkedések nélkül lehetett implementálni. Azok az osztályok

azonban, amelyekben közvetlen dinamikus memóriakezelést végezünk, azaz

amelyeknek pointer adattagjai is vannak (pointer adattagoknak közvetlen

memóriafoglalással adunk értéket, hogy aztán az így lefoglalt

memóriaterületen tárolt értékre hivatkozhassunk) csak bizonyos szabályok

betartása mellett használhatók biztonságosan.

Az egyik szabály az osztály destruktorára vonatkozik. Egy objektum

alapértelmezett megszüntetésekor egy pointer adattag, amelyik a dinamikus

memóriából általunk lefoglalt területnek címét tartalmazza, automatikusan

megszűnik ugyan, de az általa mutatott lefoglalt terület továbbra is foglalt

marad, bár már nem lesz használatban. A memóriaterületnek ez része más

célra sem használható, lényegében elveszik (ez a memória-szivárgás

jelensége). Mivel C++ nyelvben nincs automatikus felszabadító mechanizmus,

a destruktorban – amely egy objektum megszűnésekor automatikusan

meghívódik – nekünk kell gondoskodnunk az ilyen dinamikus helyfoglalások

felszabadításáról. Korábban rámutattunk a konstruktor és destruktor között

fennálló egyensúlyra (amit a konstruktor létrehoz, felépít, megnyit, azt a

destruktor lezár, lebont, megszűntet). Ez most annyiban módosul, hogy a

destruktornak nemcsak a konstruktorban végzett memóriafoglalásokat,

hanem az összes metódusban végzett memóriafoglalást fel kell szabadítania.

Minden osztály hivatalból rendelkezik egy másoló konstruktorral és

egy értékadás operátorral. Az egyik meglevő objektum másolataként

létrehoz egy új objektumot, illetve egy létező objektumot egy másik

objektummal tesz egyenlővé. Ezek e tevékenységek az adattagok szintjén

hajtódnak végre, azaz a megfelelő adattagok között kerül sor értékadásra. Ha

az adattag egy pointer, akkor az eredeti objektum ezen tagjában tárolt

memóriacím is lemásolódik és az új (értékadás esetén a másik) objektum

azonos nevű pointere ezt a címet veszi fel értékül. Ennél fogva mindkettő

objektum azonos nevű pointer tagja ugyanarra a memóriaterületre fog

mutatni, azaz két látszólag független objektum közös memóriaterületen

osztozik. Sőt értékadás esetén az értékadás baloldali objektumának a

megfelelő pointer-adattagjában tárolt korábbi cím is elvész, az azon található

adatok elérhetetlenek lesznek (adatvesztés), de foglalják a dinamikus

memóriát (memória-szivárgás).

Page 555: PROGRAMOZÁSEz 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

555

A problémára két megoldás van. Vagy megtiltjuk a másoló konstruktor

és az értékadás operátor használatát, vagy elkészítjük azok helyes változatait.

A tiltás betartását úgy szavatolhatjuk, hogy a másoló konstruktor és az

értékadás operátor deklarációját privát tagként vesszük fel az osztályba, így

ha véletlenül mégis sor kerülne valamelyik hívására, azt a fordítóprogram

felismeri és fordítási hibát fog jelezni. A másik esetben publikusnak

deklaráljuk ezeket a metódusokat, és újra kell definiálni azokat. A másoló

konstruktornak „mély másolást” kell végeznie, azaz egy pointer-adattag által

mutatott memória területen tárolt adatoknak új helyet kell foglalni, oda az

adatokat át kell másolni, és ennek az új területnek a címét kell eltárolni a

lemásolt objektum megfelelő pointer-adattagjában. Sőt, ha a lefoglalt terület

egy eleme maga is egy pointer, akkor az ő általa mutatott területet is le kell a

fenti módon másolni. A helyes értékadás operátor definíciójához egy sokszor

alkalmazható recept az, ha azt az alábbi négy lépésből álló tevékenységként

adjuk meg. Először megvizsgáljuk, hogy az értékül adott objektum nem

egyezik-e meg az értékadás baloldalán álló objektumával, röviden az

értékadás objektumával, ilyenkor ugyanis nem kell tenni semmit, önmagának

értékül adni egy objektumot nem kíván semmi tennivalót. Ha különböznek,

akkor a destruktor mintájára megszüntetjük az értékadás objektumát, majd a

másoló konstruktor mintájára újra létrehozzuk azt az értékül adandó

objektum alapján. Végül gondoskodunk arról, hogy az értékadás operátor

visszatérési értékként visszaadja az értékül adott objektum egy hivatkozását.

Page 556: PROGRAMOZÁSEz 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

556

33. Feladat: Verem

Olvassunk be a szabványos bementről egész számokat, és írjuk ki őket

fordított sorrendben a szabványos kimenetre! A megoldáshoz használjunk

vermet!

Specifikáció

A feladat lényegében egy sorozat megfordítása.

A = ( cin : ℤ*, cout : ℤ* )

Ef = ( cin =cin’ )

Uf = (

1icin

cin

1icincout '

'

' )

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(ℤ) s.Push(e) s.Pop() e:=s.Top() l:=s.Empty()

v:ℤ* v := <e, v> v :=

<v2,…,vv>

e:=v1 l:=v

Page 557: PROGRAMOZÁSEz 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

557

Absztrakt program

A megoldó program két szintre tagolódik. A felső szinten létrehozunk egy

Stack típusú objektumot, amelyre meghívhatjuk a Push(), Pop(), Top() és

Empty() műveleteket, de nem törődünk azzal, hogyan kell a vermet

megvalósítani. A verem műveletekkel, valamint a beolvasás és kiírás

műveleteivel könnyen elkészíthető a feladatot megoldó két ciklus

szekvenciája: az első feltölti a vermet, a második kiüríti.

Az alábbi struktogrammban kivételesen a C++ nyelvben megszokott

jelölésekkel hivatkozunk a beolvasás és kiírás műveleteire.

cin >> e

cin.fail()

s.Push (e)

cin >> e

s.Empty()

cout << s.Top ()

s.Pop()

A megoldás alsó szintje a verem típusának megvalósítását tartalmazza.

A verem fizikai megvalósításakor a vermet reprezentáló sorozatot kell

megfelelő módon ábrázolni a memóriában. Két klasszikus megoldást szoktak

erre alkalmazni: az egyik egy egybefüggő tömb segítségével ábrázolja a

sorozat elemeit, a másik egy fejelem nélküli láncolt listában. Az előbbi előnye

a kompakt tárolás, hátránya, hogy a tömb létrehozásakor rögzíteni kell egy

korlátot a verembe helyezendő elemek maximális számára nézve. Az utóbbi

előnye, hogy a verem méretét nem kell előre rögzíteni, hátránya viszont,

hogy a listaelemek a tárolt elem mellett egy memória címet is tartalmaznak,

így a memória igénye ennek a megoldásnak nagyobb. A veremműveletek

futási ideje azonban mindkét esetben konstans.

Page 558: PROGRAMOZÁSEz 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

558

Amikor tömbben ábrázoljuk a verem elemeit, akkor külön nyilván kell

tartanunk a legutoljára betett elem tömbindexét. Ez mutatja a verem tetején

levő elem tömbbeli helyét. Ha csak a verem tetején található értékre van

szükségünk (Top()), akkor a tömb ennyiedik elemét kell kiolvasni. Újabb érték

verembe helyezésekor (Push()) eggyel növeljük ennek a tömbindexnek az

értékét, és az így kapott helyre tesszük be az értéket. Ha az index a tömb

legutolsó elemére mutat, akkor a verem megtelt, újabb elemet nem

helyezhetünk el benne, hacsak nem másoljuk át az egészet egy nagyobb

tömbbe. Érték kivételekor (Pop()) az index értékét eggyel csökkentjük. Ha az

index a tömb előtti pozícióra mutat, akkor a verem üres (Empty()), nem lehet

belőle értéket elhagyni. Két konstruktort fogunk bevezetni. Az egyik egy 10

értéket befogadni képes tömböt fog létrehozni a verem számára, a másiknak

paraméterként lehet majd megadni a tömb méretét.

Amikor láncolt listával valósítjuk meg a vermet, akkor a verem tetején

levő értéket az első listaelem tartalmazza. Magára a listára a lista első

elemének címével hivatkozunk, amelyet egy pointer változóban tárolunk. Ha

ennek értéke nil, akkor a lista (és így a verem is) üres (Empty()), egyébként a

verem tetején levő értéket tartalmazó listaelemre mutat (Top()). Egy új érték

verembe helyezésekor (Push()) egy új listaelemet fűzünk a láncolt lista elé,

veremből való kivételkor (Pop()) – feltéve, hogy a láncolt lista nem üres –

kifűzzük a lista első elemét.

A verem típusát egy osztály segítségével definiáljuk. Ennek az

osztálynak a publikus része mindkét megvalósításnál ugyanaz kell legyen –

legfeljebb csak a konstruktorok lehetnek eltérőek. A privát rész a

reprezentációt tükrözi, ez tehát különbözik a két megvalósításnál, és

természetesen ettől függ a műveletek implementációja is. Tekintettel a

feladat egyszerű voltára, ezeket közvetlenül az C++ kód segítségével

mutatjuk be. A megvalósításban megengedjük, hogy a Pop() művelet ne csak

kivegye, hanem vissza is adja a verem tetején levő értéket, ennél fogva a

megoldó programban nem kell majd a Top() műveletet használni.

Page 559: PROGRAMOZÁSEz 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

559

Implementálás

A program komponens szerkezete

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

...

main.cpp

main()

stack.h-stack.cpp

class Stack

• Stack()

• ~Stack()

• Push()

• Pop()

• Top()

• Empty()

Page 560: PROGRAMOZÁSEz 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

560

}

kódot is alkalmazhatjuk a

cin >> i;

while(!cin.fail()){

...

cin >> i;

}

kód helyett.

A függvény első sora tömbös megvalósítású verem esetén lehetne a

kommentként megadott utasítás is. Első esetben egy előre rögzített (most

10) maximális elemszámú verem jön létre, a második – kommentként jelzett

esetben – paraméterként adható meg a verem elemszámára adott felső

korlát. A láncolt listás megvalósítás esetén a kommentben feltüntetett

utasítás nem alkalmazható.

Stack s; // Stack s(100);

int i;

while(cin >> i){

try{ s.Push(i);}

catch (Stack::Exceptions e){

if (Stack::FULLSTACK == e)

cout << "A verem megtelt\n";

}

}

Page 561: PROGRAMOZÁSEz 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

561

while(!s.Empty()){

cout << s.Pop() << endl;

}

Az első ciklusban (a verem feltöltésénél) kivételkezelést találunk.

Tömbös megvalósítás esetén a FULLSTACK kivétel akkor keletkezik, amikor a

tömb már tele van a verembe rakott elemekkel, és egy újabb elemet akarunk

betenni. (Ugyanezt a kivételt kellene lekezelni akkor is, ha a paraméteres

konstruktorral hozunk létre vermet, de a paraméterként megadott méret

olyan nagy, hogy ahhoz már nincs elég memória.) Láncolt listás megvalósítás

esetén is van létjogosultsága ennek a kivételnek, hiszen egy újabb listaelem

létrehozásánál már nem biztos, hogy van elég szabad hely az alkalmazás

számára biztosított dinamikus memóriaterületen.

Verem típus osztálya

A verem-típus megvalósítására tehát kétféle osztályt mutatunk: az egyikben

egy tömb segítségével ábrázoljuk a vermet, a másiknál ehhez egy láncolt

listát fogunk használni.

Az osztályok publikus interfésze mindkét esetben ugyanaz: a

konstruktor és destruktor mellett a szokásos verem-műveleteket kínálják fel.

Még a verem-osztály hibás felhasználás esetén dobott kivételei is azonosak a

két verzióban. Ezeket a publikus Exceptions felsorolt típus tartalmazza. A

verem akkor dob EMPTYSTACK kivételt, ha egy üres veremből ki akarunk

venni egy értéket, vagy egy üres verem tetején levő értéket akarunk

használni (Pop()és Top()műveletek). A FULLSTACK kivételnél vagy egy

tömbös reprezentációjú teli verembe akarunk újabb értéket betenni

(Push() művelet) vagy a dinamikus memóriában végrehajtott

memóriafoglalás nem sikerül (tömbös reprezentáció esetén a

konstruktorban, láncolt listás esetben a Push() műveletben).

Egyetlen apró különbséget mutat a kétféle megvalósítás publikus

része: a tömbös megvalósításhoz két konstruktort is biztosítunk.

Page 562: PROGRAMOZÁSEz 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

562

Verem típus tömbös megvalósítása

Az osztály publikus része a kivételeket, két konstruktort, a destruktort és a

verem műveletek metódusait deklarálja.

class Stack{

public:

enum Exceptions{EMPTYSTACK, FULLSTACK};

Stack();

Stack(int s);

~Stack();

void Push(int e);

int Pop();

int Top() const;

bool Empty()const;

Az osztály rejtett részében adjuk meg a verem maximális méretét

tartalmazó adattagot (size), a verem értékeit tartalmazó dinamikus

helyfoglalású tömböt (vect) és a verem tetejét jelző indexet (top).

private:

Stack(const Stack&);

Stack& operator=(const Stack&);

Page 563: PROGRAMOZÁSEz 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

563

void Allocate(int n);

int size;

int* vect;

int top;

};

A verem reprezentációja tehát már itt is dinamikus (habár az

eredendően dinamikus adatszerkezetű verem reprezentációja most statikus),

ezért az alapértelmezett másoló konstruktor és az értékadás operátor nem

működik megfelelően. (Szerencsére a főprogram nem is használja ezeket a

metódusokat.) Ha azonban egy vermet egy létező verem másolataként

hoznánk létre, akkor a másoló konstruktor nem másolná le a vect által

kijelölt dinamikusan lefoglalt tömböt, hanem ugyan azt a tömböt használná

az új verem is. Ebben a változatban nem akarunk a helyes másoló

konstruktor és értékadás operátor elkészítésével bajlódni (legyen ez az

Olvasó feladata), de hogy ne érjen bennünket meglepetés, mindkettőt privát

metódusként deklaráljuk újra. Ennél fogva, ha mégis másolni akarnánk egy

vermet, vagy egy függvénynek paraméterként (érték szerint) átadni, esetleg

értékül adni, akkor fordítási hibát kapnánk.

Az osztály két konstruktort tartalmaz. A paraméter nélküli konstruktor

beépített módon egy legfeljebb 10 elemű tömböt (azaz vermet) hoz létre, a

másiknak meg kell adni a tömb méretét. Mindkét konstruktor a privát

Allocate()függvényt hívja meg, egyik a 10-et, másik a bemenő

paraméterét adja át ennek, hogy lefoglalja a dinamikus memóriából a

megadott méretű tömböt (dinamikus helyfoglalás). Ezt követően eltárolja a

méretet a size adattagban és az üres veremre jellemző -1 kezdő értéket a

top adattagban. A destruktor feladata felszabadítani a tömböt.

Stack::Stack() { Allocate(10); }

Stack::Stack(int n) { Allocate(n); }

Page 564: PROGRAMOZÁSEz 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

564

void Stack::Allocate(int n)

{

try{

size = n;

vect = new int[n];

top = -1;

}catch(std::bad_alloc o){throw FULLSTACK;}

}

Stack::~Stack(){ delete[] vect;}

A Push() művelet megnöveli a top értékét – feltéve, hogy így nem

lépi át a tömb indextartományának felső határát, mert különben FULLSTACK

kivételt dob – és a vect tömb top-adik pozíciójára beírja az új elemet. A

Pop()művelet kiolvassa a vect tömb top-adik pozícióján található elemet,

majd csökkenti a top értékét eggyel – feltéve, hogy a top nem -1, mert

ekkor EMPTYSTACK kivételt dob a metódus. A Top()művelet üres lista

esetén EMPTYSTACK kivételt dob, egyébként a vect tömb top-adik

pozícióján található elemet. Az Empty()művelet akkor ad igaz értéket, ha

top értéke -1, egyébként hamis értékkel tér vissza. A Top() és az

Empty()műveletek konstans metódusok, hiszen működésük során nem

módosul a verem objektum.

void Stack::Push(int e)

{

if( top+1 == size ) throw FULLSTACK;

vect[++top]=e;

}

Page 565: PROGRAMOZÁSEz 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

565

int Stack::Pop()

{

if( -1 == top ) throw EMPTYSTACK;

return vect[top--];

}

int Stack::Top() const

{

if( -1 == top ) throw EMPTYSTACK;

return vect[top];

}

bool Stack::Empty()const

{

return -1 == top;

}

Verem típus egyirányú fejelem nélküli láncolt listás megvalósítása

Az osztály publikus része egyetlen részletben különbözik az előző

megoldástól: egyetlen paraméter nélküli konstruktora van. Most sort

kerítünk a másoló konstruktor és az értékadás operátor implementációjára

annak ellenére, hogy a főprogram nem használja ezeket. Ezek

alapértelmezett változatai ugyanis rosszul működnének, ha mégis szükség

lenne rájuk.

class Stack{

public:

enum Exceptions{EMPTYSTACK, FULLSTACK};

Page 566: PROGRAMOZÁSEz 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

566

Stack();

~Stack();

Stack(const Stack&);

Stack& operator=(const Stack&);

void Push(int e);

int Pop();

int Top() const;

bool Empty()const;

Az osztály rejtett részében definiáljuk a listaelem típusát. Ezt követi a

verem reprezentációja, ami nem más, mint a legelső listaelemre mutató

head pointer.

private:

struct Node{

int val;

Node *next;

Node(int e, Node *n) : val(e), next(n){}

};

Node *head;

};

Page 567: PROGRAMOZÁSEz 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

567

A konstruktor egy üres vermet, tehát egy üres láncolt listát inicializál,

ehhez beállítja a head értékét nil-re. A destruktor felszabadítja a vermet

megvalósító lista összes listaelemét:

Stack::Stack(): head(NULL){}

Stack::~Stack()

{

Node *p;

while(head != NULL){

p = head;

head = head->next;

delete p;

}

}

A Push()művelet létrehoz egy új listaelemet, és befűzi azt a lista

elejére. Ha nem lehet újabb listaelemet lefoglalni, akkor egy memória-

foglalá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){

Page 568: PROGRAMOZÁSEz 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

568

throw FULLSTACK;

}

}

A Pop() művelet üres lista esetén EMPTYSTACK kivételt dob,

egyébként kifűzi a lista legelső elemét, majd felszabadítja azt, de visszaadja a

kifűzött listaelem értékét a hívása helyére.

int Stack::Pop()

{

if(NULL == head) throw EMPTYSTACK;

int e = head->val;

Node *p = head;

head = head->next;

delete p;

return e;

}

A Top()művelet üres lista esetén EMPTYSTACK kivételt dob,

egyébként visszaadja a lista legelső elemében tárolt értéket. Az Empty()

művelet üres lista esetén ad igaz értéket, egyébként hamis értékkel tér

vissza.

int Stack::Top()const

{

if(NULL == head) throw EMPTYSTACK;

Page 569: PROGRAMOZÁSEz 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

569

return head->val;

}

bool Stack::Empty()const

{

return NULL == head;

}

A másoló konstruktor üres inicializáló verem-objektum esetén egy üres

láncolt listát, egyébként egy új láncolt listát épít fel. Ez utóbbiban

ugyanazokat az értékeket, ugyanolyan sorrendben helyezi el, mint amelyek

lemásolandó verem láncolt listájában vannak, tehát egy lista-másolást végez.

Stack::Stack(const Stack& s)

{

if(NULL == s.head) head = NULL;

else {

try{

head = new Node(s.head->val,NULL);

}catch(std::bad_alloc o){

throw FULLSTACK;

}

Node *q = head;

Node *p = s.head->next;

while(p != NULL){

try{ q->next = new Node(p->val,NULL);}

Page 570: PROGRAMOZÁSEz 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

570

catch(std::bad_alloc o){throw FULLSTACK;}

q = q->next;

p = p->next;

}

}

}

Az értékadás operátor – feltéve, hogy az értékadás két oldalán

található verem-objektumok nem azonosak – az alapértelmezett verem

objektum (ez az értékadás baloldali objektuma) által foglalt láncolt lista

törléséből, majd az értékül adandó verem objektum láncolt listájának

lemásolásából áll. Mindez kiegészül a megfelelő visszatérési érték

megadásával.

Stack& Stack::operator=(const Stack& s)

{

if(&s == this) return *this;

Node *p;

while(head != NULL){

p = head;

head = head->next;

delete p;

}

if(NULL == s.head) head = NULL;

Page 571: PROGRAMOZÁSEz 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

571

else {

try{ head = new Node(s.head->val,NULL);}

catch(std::bad_alloc o){throw FULLSTACK;}

Node *q = head;

Node *p = s.head->next;

while(p != NULL){

try{ q->next = new Node(p->val,NULL);}

catch(std::bad_alloc o){throw FULLSTACK;}

q = q->next;

p = p->next;

}

}

return *this;

}

Megfigyelhető, hogy az értékadás operátor lényegében a

destruktorban és a másoló konstruktorban leírt kód szekvenciája, amely

kiegészül egy kezdeti vizsgálattal (&s==this) és a végén egy speciális

return *this utasítással.

Page 572: PROGRAMOZÁSEz 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

572

Tesztelés

Az alapfeladat érvényes fekete doboz tesztelése nem igényel túl sok

tesztesetet. Egy általános bemenettel ellenőrizhetjük, hogy a kimenten

fordított sorrendben jelennek-e meg az értékek. Természetesen kipróbáljuk

nulla darab és egyetlen értékből álló bemeneteket is.

Az érvénytelen tesztesetek kivételt eredményeznek. A főprogram nem

ad lehetőséget az EMPTYSTACK kivétel keletkezésére, ezért érdemes erre

egy külön tesztprogramot készíteni.

Stack s;

try{ s.Top();}

catch (Stack::Exceptions e){

if (Stack::EMPTYSTACK == e)

cout << "A verem ures!\n";

}

A FULLSTACK kivétel könnyen kiváltható, ha a tömbös megvalósítású

verem esetén tíznél több elemet akarunk elhelyezni a veremben. Azt is illik

azonban kimérni, hogy legfeljebb mekkora tömböt lehet létrehozni.

try{

int n;

while(true){

cout << "A verem mérete: "; cin >> n;

Stack s(n);

}

}catch (Stack::Exceptions e){

Page 573: PROGRAMOZÁSEz 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

573

if (Stack::FULLSTACK == e)

cout << "Nincs elég memória!\n";

}

Láncolt listás megvalósítású verem esetben is egy speciális (nem

interaktív) főprogramot készítünk ahhoz, hogy a „verem betelt” hibaüzenetet

megkapjuk.

Page 574: PROGRAMOZÁSEz 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

574

Stack s;

int i;

try{

while(true){ s.Push(i++); }

}catch (Stack::Exceptions e){

if (Stack::FULLSTACK == e)

cout << "Nincs elég memória!"

<< "Listaelemek szama:" << i << endl;

}

Az eredeti főprogram viszont nem alkalmas arra, hogy a Stack osztály

komponens tesztjét elvégezzük. Ehhez célszerű egy menüválasztós

tesztprogramot készíteni, amely lehetővé teszi ennek a komponensnek a

fekete és fehér doboz tesztelését. Célszerű egy olyan metódussal is

kiegészíteni az osztályt, amelyik meg tudja jeleníteni a verembe levő értékek

sorozatát. Ezt végrehajtva minden művelet előtt és után pontos képet

nyerhetünk a művelet működéséről. A fehér doboz teszthez hasznos lehet,

ha ez a kiírás nemcsak a verembe levő elemek sorozatát adja meg, hanem

annak ábrázolásával kapcsolatos egyéb adatokat is (tömbös megvalósításnál

a top adattag értékét, láncolt listás esetben a pointer értékeket).A

műveletek önmagukban véve igen egyszerűek, az Olvasó könnyen kitalálhat

rájuk tesztadatokat. Ezen kívül a komponens teszt a műveletek variációs

tesztjét is tartalmazza. Ilyen variációk lehetnek az alábbiak:

1. Ha üres, vagy bármilyen más veremre többször alkalmazzuk a

Push() műveletet, majd ugyanannyiszor a Pop() műveletet, akkor

vissza kell kapnunk a kiindulási vermet.

2. A Push() vagy a Pop() művelet egymás után nem hajtható végre

akárhányszor ugyanazzal az eredménnyel.

Page 575: PROGRAMOZÁSEz 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

575

3. A Top() vagy az Empty() művelet egymás után akárhányszor

végrehajtható, eredménye nem változik.

Page 576: PROGRAMOZÁSEz 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

576

Teljes program

main.cpp:

#include <iostream>

#include "stack.h"

using namespace std;

int main()

{

Stack s;

int i;

while(cin >> i){

try{ s.Push(i);}

catch (Stack::Exceptions e){

if (Stack::FULLSTACK == e)

cout << "A verem megtelt\n";

}

}

while(!s.Empty()){

cout << s.Pop() << endl;

}

return 0;

}

stack.h: (tömbös változat)

Page 577: PROGRAMOZÁSEz 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

577

#ifndef STACK_H

#define STACK_H

class Stack{

public:

enum Exceptions{EMPTYSTACK, FULLSTACK};

Stack();

Stack(int s);

~Stack();

void Push(int e);

int Pop();

int Top() const;

bool Empty()const;

private:

Stack(const Stack&);

Stack& operator=(const Stack&);

void Allocate(int n);

int size;

int* vect;

Page 578: PROGRAMOZÁSEz 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

578

int top;

};

#endif

stack.cpp: (tömbös változat)

#include "stack.h"

#include <memory>

Stack::Stack() { Allocate(10); }

Stack::Stack(int n) { Allocate(n); }

void Stack::Allocate(int n)

{

try{

size = n;

vect = new int[n];

top = -1;

}catch(std::bad_alloc o){throw FULLSTACK;}

}

Stack::~Stack()

{

delete[] vect;

}

void Stack::Push(int e)

Page 579: PROGRAMOZÁSEz 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

579

{

if( top+1 == size ) throw FULLSTACK;

vect[++top]=e;

}

int Stack::Pop()

{

if( -1 == top ) throw EMPTYSTACK;

return vect[top--];

}

int Stack::Top() const

{

if( -1 == top ) throw EMPTYSTACK;

return vect[top];

}

bool Stack::Empty()const

{

return -1 == top;

}

stack.h: (láncolt listás változat)

#ifndef STACK_H

#define STACK_H

class Stack{

public:

Page 580: PROGRAMOZÁSEz 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

580

enum Exceptions{EMPTYSTACK, FULLSTACK};

Stack();

~Stack();

Stack(const Stack&);

Stack& operator=(const Stack&);

void Push(int e);

int Pop();

int Top() const;

bool Empty()const;

private:

struct Node{

int val;

Node *next;

Node(int e, Node *n) : val(e), next(n){}

};

Node *head;

};

#endif

Page 581: PROGRAMOZÁSEz 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

581

Page 582: PROGRAMOZÁSEz 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

582

stack.cpp: (láncolt listás változat)

#include "stack.h"

#include <memory>

using namespace std;

Stack::Stack(): head(NULL){}

Stack::~Stack()

{

Node *p;

while(head != NULL){

p = head;

head = head->next;

delete p;

}

}

void Stack::Push(int e)

{

try{ head = new Node(e,head);}

catch(std::bad_alloc o){ throw FULLSTACK;}

}

int Stack::Pop()

{

if(NULL == head) throw EMPTYSTACK;

int e = head->val;

Page 583: PROGRAMOZÁSEz 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

583

Node *p = head;

head = head->next;

delete p;

return e;

}

int Stack::Top()const

{

if(NULL == head) throw EMPTYSTACK;

return head->val;

}

bool Stack::Empty()const

{

return NULL == head;

}

Page 584: PROGRAMOZÁSEz 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

584

Stack::Stack(const Stack& s)

{

if(NULL == s.head) head = NULL;

else {

try{

head = new Node(s.head->val,NULL);

}catch(std::bad_alloc o){

throw FULLSTACK;

}

Node *q = head;

Node *p = s.head->next;

while(p != NULL){

try{ q->next = new Node(p->val,NULL);}

catch(std::bad_alloc o){throw FULLSTACK;}

q = q->next;

p = p->next;

}

}

}

Stack& Stack::operator=(const Stack& s)

{

if(&s == this) return *this;

Page 585: PROGRAMOZÁSEz 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

585

Node *p;

while(head != NULL){

p = head;

head = head->next;

delete p;

}

if(NULL == s.head) head = NULL;

else {

try{ head = new Node(s.head->val,NULL);}

catch(std::bad_alloc o){throw FULLSTACK;}

Node *q = head;

Node *p = s.head->next;

while(p != NULL){

try{ q->next = new Node(p->val,NULL);}

catch(std::bad_alloc o){throw FULLSTACK;}

q = q->next;

p = p->next;

}

}

return *this;

}

Page 586: PROGRAMOZÁSEz 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

586

34. Feladat: Kettős sor

A szabványos bemenetről érkező egész számokat szortírozzuk és írjuk ki a

szabványos kimenetre úgy, hogy először a negatívokat, majd azt követően a

többit jelenítjük meg! Ezen kívül minden kiírt szám mellé odaírjuk azt is, hogy

az hányszor szerepelt a bemeneti értékek között! A feladat megoldásához

használjunk egy kettős sort!

Specifikáció

A feladat megoldását két szakaszra bontjuk. Először a bemenő értékek

sorozatát alakítjuk át úgy, hogy az elején legyenek a negatív számok, a végén

pedig a többi. (Kényelmesebb a specifikációban a bemenő sorozatot egy

felsoroló segítségével elérni: ezt jelöli a cin.) Az első szakasz eredményeként

létrejött t sorozat elemeit a második szakaszban bejárjuk, és minden elemére

megszámoljuk, hogy az hányszor szerepel ebben a sorozatban. Ehhez a t

sorozat kétszintű bejárására van szükség, hiszen az elemek bejárása közben

minden elemre egy újabb bejárással kell a számlálást megvalósítani.

A = ( cin : enor(ℤ), cout : ℤ* )

Ef = ( cin =cin’ )

Uf = ( )()(''

eet

0ecine

0ecine

),(

edtdte

ecout 1 )

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:

Page 587: PROGRAMOZÁSEz 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

587

Loext() : egy elemi érték berakása a sor elejére

Lopop() : egy elemi érték levétele a sor elejéről

Hiext() : egy elemi érték berakása a sor végére

Hipop() : egy elemi érték levétele a sor végéről

b:BiQueue(ℤ) b.Loext(e)

b.Hiext(e)

b.Lopop() b.Hipop()

t : ℤ* t := <e, t>

t := <t, e>

t := <t2,…,tt>

e := t1

t := <t1,…, tt–1>

e := tt

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.

Page 588: PROGRAMOZÁSEz 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

588

Absztrakt program

A megoldó programnak két szintje lesz. A felső szinten létrehozunk egy

kettős sort, és az előírt módon feltöltjük a Loext() és Hiext() műveleteinek

segítségével. Ezután létrehozunk a kettős sorhoz több felsoroló objektumot

és azok First(), Next(), End() és Current() műveleteire támaszkodva

elvégezzük az elemeknek a számlálásokat is magába foglaló kiíratását.

A megoldás tehát két ciklus szekvenciája lesz. Az elsőben a C++

nyelvben megszokott jelölésekkel hivatkozunk a beolvasásra. A cin.fail()

feltétel azt jelöli, hogy a legutolsó olvasás sikeres volt, van még

feldolgozatlan bemeneti érték.

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() db := előfordul(e)

it1.First() it2 = b.CreateEnumerator()

it1.End() it2.First();db:=0

e:=it1.Current() it2.End()

db := előfordul(e) it2.Current()=e

cout << e << db db:=db+1 SKIP

Page 589: PROGRAMOZÁSEz 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

589

it1.Next() it2.Next()

A megoldás alsó szintje a kettős sor típusának és a kettős sor

felsorolója típusának megvalósítását tartalmazza. Mindkettőre egy-egy

osztályt hozunk létre.

A kettős sort egy fejelem nélküli kétirányú láncolt listával fogjuk

reprezentálni. Két pointerrel hivatkozunk majd erre a listára:

first: lista első elemére mutat (üres lista esetén nil)

last: lista utolsó elemére mutat (üres lista esetén nil)

A kettős sor műveleteit a láncolt lista elejére és végére történő

beszúrás, és onnan való törlés implementálja. Ezen műveletek mellé egy

speciális metódust is felveszünk: a CreateEnumerator() segítségével tudunk

majd a kettős sorhoz egy új felsoroló objektumot létrehozni.

A felsoroló objektumot két pointerrel reprezentáljuk. Az egyik arra a

kettős sorra mutat, amelyik elemeit felsoroljuk, a másik a kettős sort

reprezentáló láncolt lista azon listaelemére mutat majd, amelyik a felsorolás

során érintett aktuális elem.

Implementálás

A program komponens szerkezete

Page 590: PROGRAMOZÁSEz 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

590

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

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

Page 591: PROGRAMOZÁSEz 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

591

}

A beolvasás után bejárjuk a sort, és kiírjuk az elemeit a szabványos

kimenetre, közben minden elemre megszámoljuk, hányszor van benne a

sorban. Ehhez egyidejűleg két felsorolót használunk. A BiQueue osztályba

ágyazott Enumerator osztályra a BiQueue::Enumerator-ral

hivatkozhatunk.

BiQueue::Enumerator it1 = x.CreateEnumerator();

for(it1.First(); !it1.End(); it1.Next()){

i = it1.Current();

int s = 0;

BiQueue::Enumerator it2

= x.CreateEnumerator ();

for(it2.First(); !it2.End(); it2.Next()){

if (it2.Current() == i) ++s;

}

cout << i << " előfordulásainak száma: "

<< s << endl;

}

return 0;

A kettős sor osztálya

Az Exceptions felsorolt típus azt az értéket tartalmazza, amelyet a

kettős sor hibás felhasználás esetén kivételként dob. Jelen esetben ez akkor

Page 592: PROGRAMOZÁSEz 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

592

következik be, ha egy üres sorból ki akarunk venni egy értéket (ld. Lopop()

és Hipop() műveleteknél).

class BiQueue{

public:

enum Exceptions{EMPTYSEQ};

BiQueue():

first(NULL),last(NULL){}

~BiQueue();

void Loext(int e);

int Lopop();

void Hiext(int e);

int Hipop();

BiQueue(const BiQueue&);

BiQueue& operator=(const BiQueue&);

A konstruktort inline módon definiáltuk. Ez egy üres kettős sort, azaz

egy üres láncolt listát hoz létre úgy, hogy a két privát pointert NULL-ra állítja.

Mind a másoló konstruktort, mind az értékadás operátort újradefiniáljuk.

private:

struct Node{

int val;

Node *next;

Page 593: PROGRAMOZÁSEz 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

593

Node *prev;

Node(int c, Node *n, Node *p):

val(c), next(n), prev(p){};

};

Node *first;

Node *last;

A privát részben adjuk meg a listaelemek típusát. Ez egy három részből

(érték és két pointer) álló struktúra (Node), amelynek konstruktorával

hozhatunk létre egy új listaelemet. E struktúra ismeretében definiáljuk a

láncolt lista legelső illetve legutolsó listaelemére mutató first és last

pointereket. Ez a két pointer lényegében a kettős sor reprezentációja.

Ezzel a BiQueue osztály definícióját még nem fejeztük be. Hiányzik a

CreateEnumerator() metódus deklarációja, de ehhez előbb szükség van

az Enumerator osztály definíciójára.

Felsoroló a kettős sorhoz

Egy kettős sor felsorolását megvalósító osztályt (class Enumerator) a

BiQueue osztály beágyazott publikus osztályaként definiáljuk. Ez egyrészt

egyértelművé teszi, hogy itt a BiQueue osztály felsorolójáról lesz szó,

másrészt az Enumerator osztály metódusai hivatkozhatnak a BiQueue

osztály privát elemeire is.

Egy felsorolót elsősorban az a current pointer reprezentálja, amelyet

a kettős sor láncolt listáján tudunk végigvezetni és a bejárás során mindig az

aktuális listaelemre mutat. Ezen kívül a felsoroló magára a bejárni kívánt

kettős sorra is hivatkozik egy pointer segítségével (bq). Az Enumerator

osztály metódusai: First(), Next(), Current(), End(). Ezeknek a

műveleteknek az implementációja igen egyszerű (éppen ezért inline módon

adjuk meg), lényegében a kettős sort reprezentáló láncolt listával és a

current pointerrel operálnak. A First() ráállítja a current pointert a

lista első elemére, a Next() a következőre, az End() akkor ad igazat, ha a

Page 594: PROGRAMOZÁSEz 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

594

current értéke már NULL (azaz lefutott a listáról), a Current() pedig a

current pointer által mutatott listaelem értékét adja vissza.

class Enumerator{

public:

Enumerator(BiQueue *p): bq(p),current(NULL){};

int Current()const {return current->val;}

void First() {current = bq->first;}

bool End() const {return NULL == current;}

void Next() {current = current->next;}

private:

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;

Page 595: PROGRAMOZÁSEz 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

595

return it;}

Enumerator& operator++(){ // ++it

current = current->next;

return *this;}

Egy BiQueue::Enumerator it(&x) utasítás segítségével

hozhatunk létre egy it nevű felsorolót az x kettős sorhoz. Ezt a műveletet a

kettős sor korábban már beígért CreateEnumerator() metódusával is

elvégezhetjük. Térjünk tehát vissza a kettős sor osztályának definíciójához.

A kétirányú sor osztálydefiníciójának folytatása

Most már felvehetjük a BiQueue osztályba a CreateEnumerator()

publikus metódust, amelynek implementációját inline módon adjuk meg.

Enumerator CreateEnumerator()

{ return Enumerator(this); }

Az BiQueue osztály definíciója ezzel el is készült. Hátra van még a

korábban deklarált műveleteknek a megvalósítása.

A destruktor felszabadítja a kettős sort ábrázoló láncolt listát. Itt is

látható, hogy míg a konstruktor egyáltalán nem végez memóriafoglalást,

addig a destruktor számos listaelemet felszabadíthat, hiszen a kettős sor

használata során a Loext() és Hiext() műveletek dinamikusan hozzák

létre a listaelemeket.

BiQueue::~BiQueue(){

Node *p, *q;

q = first;

Page 596: PROGRAMOZÁSEz 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

596

while( q != NULL){

p = q;

q = q->next;

delete p;

}

}

A másoló konstruktor egy p pointert vezet végig a lemásolandó kettős

soron (feltéve, hogy az nem üres), és az új kettős sor listájának felépítéséhez

egy q pointert használ.

BiQueue::BiQueue(const BiQueue &b){

if(NULL == b.first){

first = last = NULL;

}else{

Node *q = new Node(b.first->val,NULL,NULL);

first = q;

for(Node *p=b.first->next;

p != NULL;p=p->next){

q = new Node(p->val,NULL,q);

q->prev->next = q;

}

last = q;

}

}

Page 597: PROGRAMOZÁSEz 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

597

Az értékadás operátort az alábbi séma mintájára definiáljuk.

BiQueue& BiQueue::operator=(const BiQueue &s){

if(&s == this) return *this;

// destruktor

// másoló konstruktor

return *this;

}

Miután megvizsgálja, hogy az alapértelmezés szerinti (értékadás

baloldali) objektuma különbözik-e az értékül adandó objektumtól, előbb a

destruktornak megfelelően felszabadítja az alapértelmezés szerinti objektum

memóriafoglalásait, majd a másoló konstuktorhoz hasonlóan létrehozza az új

kettős sort.

BiQueue& BiQueue::operator=(const BiQueue &s){

if(&s == this) return *this;

Node *p = first;

while(p != NULL){

Node *q = p->next;

delete p;

p = q;

}

if(NULL == s.first){

first = last = NULL;

}else{

Page 598: PROGRAMOZÁSEz 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

598

Node *q = new Node(s.first->val,NULL,NULL);

first = q;

for(Node *p=s.first->next;

p != NULL;p=p->next){

q = new Node(p->val,NULL,q);

q->prev->next = q;

}

last = q;

}

return *this;

}

A Loext() illetve a Hiext() művelet létrehoz egy új listaelemet,

kitölti annak értékét, és befűzi a lista elejére illetve a végére, és ennek

megfelelően állítjuk a first vagy a last pointert. Amikor a legelső elemet

fűzzük be az üres listába, akkor mindkét esetben mindkét pointert állítani

kell.

void BiQueue::Loext(int e){

Node *p = new Node(e,first,NULL);

if(first != NULL) first->prev = p;

first = p;

if(NULL == last) last = p;

}

void BiQueue::Hiext(int e){

Page 599: PROGRAMOZÁSEz 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

599

Node *p = new Node(e,NULL,last);

if(last != NULL) last->next = p;

last = p;

if(NULL == first) first = p;

}

A Lopop() művelet EMPTYSEQ kivételt dob, ha a lista üres, egyébként

kifűzi a lista legelső elemét, a benne tárolt értéket elmenti, és a listaelemet

felszabadítja. Ha a lista eredetileg egyelemű volt, akkor az utolsó elemre

mutató last pointert NULL-ra kell állítani. A Hipop() a Lopop() duálisa.

int BiQueue::Lopop(){

if(NULL == first) throw EMPTYSEQ;

int e = first->val;

Node *p = first;

first = first->next;

delete p;

if(first != NULL) first->prev = NULL;

else last = NULL;

return e;

}

int BiQueue::Hipop(){

if(NULL == last)throw EMPTYSEQ;

int e = last->val;

Node *p = last;

Page 600: PROGRAMOZÁSEz 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

600

last = last->prev;

delete p;

if(last != NULL) last->next = NULL;

else first = NULL;

return e;

}

Elem törlése bejárás közben

Egy problémával érdemes még foglalkoznunk annak ellenére, hogy a fenti

alkalmazásban ennek nincs szerepe. A kettős sor elemeinek felsorolása

ugyanis elromolhat, ha bejárás során a sorból törlünk egy olyan elemet,

amelyre éppen a felsoroló current pointere hivatkozik.

A probléma elkerülésére több megoldás is elképzelhető:

Teljes kizárás: (Ezt a változatot építettük be az alább látható

teljes programba.) A kettős sor reprezentációjában

nyilvántartjuk a létrehozott felsorolók számát (ez új kettős

sornál kezdetben nulla), amit a felsoroló konstruktora növel,

destruktora csökkent, és ha ez a számláló nem nulla, akkor a

kettős sor Lopop() és Hipop() törlőműveletei egy speciális

kivételt dobnak.

Elemszintű kizárás: A törlő műveletek csak akkor dobnak

kivételt, ha olyan listaelemet törölnének, amelyre valamelyik

felsoroló éppen hivatkozik. Ehhez nyilván kell tartani a kettős sor

reprezentációjában a létrehozott felsorolókat, és végig kell

vizsgálni azokat a törlés előtt.

Törlés késleltetés: Ha a törlendő elemre éppen egy felsoroló

mutat, akkor annak törlését elhalasztjuk. Ehhez meg kell jelölni a

törlendő elemet, és amikor egyik felsoroló sem mutat erről

listaelemre, akkor törölhetjük. A megvalósításhoz itt is nyilván

kell tartani a kettős sor reprezentációjában a létrehozott

Page 601: PROGRAMOZÁSEz 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

601

felsorolókat, a listaelemeket pedig ki kell egészíteni egy törlést

jelző mezővel. Egy törlés újbóli kísérletét a felsorolók Next()

műveletének végrehajtásához köthetjük.

Tesztelés

Az alapfeladat érvényes fekete doboz tesztelését kipróbálhatjuk nulla darab

bemenettel, majd egy darabbal, majd csupa különböző értékkel (legyenek

köztük negatív és nem negatív értékek is), csupa azonos értékkel, végül egy

általános esettel.

Az érvénytelen tesztesetek a kivétel dobások kikényszerítésére

irányulnak. A főprogram azonban nem ad lehetőséget az EMPTYSTACK

kivétel dobására, hiszen nem törlünk. Érvénytelen teszteset az is, amikor

valamelyik new utasítás bad_alloc kivételt dob. Ennek kikényszerítésére

sem alkalmas a főprogramunk, az előző feladatnál látott tesztprogramot

kellene most is elkészíteni.

A BiQueue osztály komponens tesztjéhez most is egy menüválasztós

tesztprogramot kell készíteni. A műveletek variációs tesztjéhez a teljesség

igénye nélkül néhány példa:

1. Ha üres, vagy bármilyen más sorra többször alkalmazzuk a Hiext()

műveletet, majd ugyanannyiszor a Lopop() műveletet, akkor

visszakapjuk a kiindulási sort.

2. A Hiext(), Loext(), Hipop(), Lopop() művelet bármelyike

egymás után nem hajtható végre akárhányszor ugyanazzal az

eredménnyel.

3. Kezdetben üres sorra végrehajtott Hiext(1), Hiext(2),

First(), Next(), Hipop() után „törlés bejárás közben” kivétel

keletkezik.

Page 602: PROGRAMOZÁSEz 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

602

Teljes program

main.cpp:

#include <iostream>

#include "biqueue.h"

using namespace std;

int main()

{

BiQueue x;

int i;

while(cin >> i){

if (i>0) x.Hiext(i);

else x.Loext(i);

}

BiQueue:: Enumerator it1 = x.CreateEnumerator();

for(it1.First(); !it1.End(); it1.Next()){

i = it1.Current();

int s = 0;

BiQueue:: Enumerator it2

= x.CreateEnumerator ();

for(it2.First(); !it2.End(); it2.Next()){

if (it2.Current() == i) ++s;

Page 603: PROGRAMOZÁSEz 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

603

}

cout << i << " előfordulásainak száma: "

<< s << endl;

}

return 0;

}

Page 604: PROGRAMOZÁSEz 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

604

biqueue.h:

#ifndef BIQUEUE_H

#define BIQUEUE_H

#include <memory>

class BiQueue{

public:

enum Exceptions{EMPTYSEQ, UNDERTRAVERSAL};

BiQueue():

first(NULL),last(NULL),enumeratorCount (0){}

BiQueue(const BiQueue&);

BiQueue& operator=(const BiQueue&);

~BiQueue();

void Loext(int e);

int Lopop();

void Hiext(int e);

int Hipop();

private:

struct Node{

int val;

Node *next;

Node *prev;

Node(int c, Node *n, Node *p):

val(c), next(n), prev(p){};

Page 605: PROGRAMOZÁSEz 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

605

};

Node *first;

Node *last;

int enumeratorCount;

public:

class Enumerator{

public:

Enumerator(BiQueue *p):bq(p),current(NULL)

{++(bq->enumeratorCount);}

~Enumerator(){--(bq->enumeratorCount);}

int Current()const {return current->val;}

void First() {current = bq->first;}

bool End() const {return NULL == current;}

void Next() {current = current->next;}

private:

BiQueue *bq;

Node *current;

};

Enumerator CreateEnumerator()

{return Enumerator(this);}

};

#endif

biqueue.cpp:

Page 606: PROGRAMOZÁSEz 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

606

#include "biqueue.h"

using namespace std;

BiQueue::~BiQueue(){

Node *p, *q;

q = first;

while( q != NULL){

p = q;

q = q->next;

delete p;

}

}

BiQueue::BiQueue(const BiQueue &s){

if(NULL == s.first)first = last = NULL;

else{

Node *q = new Node(s.first->val,NULL,NULL);

first = q;

for(Node *p=s.first->next;

p != NULL;p=p->next){

q = new Node(p->val,NULL,q);

q->prev->next = q;

}

last = q;

}

Page 607: PROGRAMOZÁSEz 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

607

}

BiQueue& BiQueue::operator=(const BiQueue &s){

if(&s == this) return *this;

Node *p = first;

while(p != NULL){

Node *q = p->next;

delete p;

p = q;

}

if(NULL == s.first) first = last = NULL;

else{

Node *q = new Node(s.first->val,NULL,NULL);

first = q;

for(Node *p=s.first->next;

p != NULL;p=p->next){

q = new Node(p->val,NULL,q);

q->prev->next = q;

}

last = q;

}

return *this;

}

Page 608: PROGRAMOZÁSEz 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

608

void BiQueue::Loext(int e){

Node *p = new Node(e,first,NULL);

if(first != NULL) first->prev = p;

first = p;

if(NULL == last) last = p;

}

int BiQueue::Lopop(){

if(enumeratorCount != 0) throw UNDERTRAVERSAL;

if(NULL == first) throw EMPTYSEQ;

int e = first->val;

Node *p = first;

first = first->next;

delete p;

if(first != NULL) first->prev = NULL;

else last = NULL;

return e;

}

void BiQueue::Hiext(int e){

Node *p = new Node(e,NULL,last);

if(last != NULL) last->next = p;

last = p;

if(NULL == first) first = p;

}

int BiQueue::Hipop(){

Page 609: PROGRAMOZÁSEz 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

609

if(enumeratorCount != 0) throw UNDERTRAVERSAL;

if(NULL == last)throw EMPTYSEQ;

int e = last->val;

Node *p = last;

last = last->prev;

delete p;

if(last != NULL) last->next = NULL;

else first = NULL;

return e;

}

Page 610: PROGRAMOZÁSEz 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

610

C++ kislexikon

pointer int* p;

dinamikus

helyfoglalás

és törlés

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;

Page 611: PROGRAMOZÁSEz 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

611

}

lista

felépítése

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;

}

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;

}

lista

lebontása

while(h!=NULL){

Node *p = h;

h = p->next;

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

}

Page 612: PROGRAMOZÁSEz 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

612

14. Objektum-orientált kód-újrafelhasználási technikák

Egy program költségét több szempont alapján határozhatjuk meg: ilyenek a

futási idő, a memória igény, a programkód előállítási költsége, a hibajavítás

és karbantartás költsége. Például nemcsak elegáns, de az előállítási és a

hibajavítási költségen is javít, ha a programkód nem tartalmaz ismétlődő

részeket, és egy többször is felhasználandó kódrészt csak egyszer írunk le a

kódban, majd azokon a helyeken, ahol szükség van rá, csak felhasználjuk. Ha

ráadásul egy ilyen kódrész önálló csomagban (komponensben) helyezkedik

el, akkor más alkalmazásokban könnyen újra fel tudjuk majd használni.

A kód ilyen újrafelhasználását szolgálják a korábban már ismertetett

alprogramok, amelyek egy-egy részprogram sokszoros felhasználására adnak

lehetőséget. Ide sorolható az is, amikor azonos tulajdonságú objektumok

közös leírására osztályokat definiálunk. Ebben a fejezetben olyan további

nyelvi eszközöket mutatunk be, amelyekkel egy osztály definíciójában leírt

kódot újra fel lehet használni, de úgy, hogy lehetőségünk legyen az

újrafelhasznált kódon némiképp változtatni, hozzászabni azt a konkrét

alkalmazás céljaihoz.

A származtatás az újrafelhasználás objektum orientált nyelvi eszköze.

Ennek keretében egy osztályt – az utódosztályt – egy már létező másik

osztály – az ősosztály – mintájára, az ősosztályhoz hasonlóra definiálhatunk

úgy, hogy az utódosztály megkapja, örökli az ősosztály tulajdonságait, azaz

rendelkezni fog az ősosztály adattagjaival és metódusaival, de az örökölt

tagokon kívül felruházhatjuk egyéb tulajdonságokkal is: kiegészíthetjük újabb

adattagokkal és metódusokkal, felüldefiniálhatjuk az ősosztály bizonyos

metódusait.

Az újrafelhasználás másik nyelvi eszköze a sablon (template, generic)

példányosítása. Most elsősorban osztály-sablonokról lesz szó, amikor egy

osztály definícióját úgy adjuk meg, hogy abban bizonyos elemeket nem

konkretizálunk, csak később megadandó paraméterekkel jelölünk. Ilyen lehet

például egy olyan verem-típust megvalósító osztály, ahol a verembe betenni

kívánt elemek típusa még ismeretlen, azt egy paraméter helyettesíti. Amikor

egy ilyen osztályt fel akarunk használni (objektumot készítünk a mintájára)

Page 613: PROGRAMOZÁSEz 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

613

akkor először ezeket a sablon-paramétereket kell konkrét elemekkel

helyettesíteni, azaz az osztály-sablonból létre kell hoznunk, példányosítanunk

kell egy konkrét osztályt.

Mindkét nyelvi eszköz azt támogatja, hogy közös tulajdonsággal

rendelkező osztályok használata esetén, az osztályok hasonló elemeit csak

egyszer kelljen megadni, leírni, és azokat újra és újra felhasználni.

Implementációs stratégia

Az, hogy a programunkban használt osztályok előállításához kell-e

ősosztályokat és/vagy osztály-sablonokat használni, sokszor már a tervezés

során kiderül, hiszen már ekkor felismerhetjük, ha egy feladat megoldásában

részt vevő különböző típusok hasonlítanak egymásra, és kísérletet tehetünk a

hasonló tulajdonságok közös leírására. Előfordul azonban az is, hogy

minderre csak az implementáció során figyelünk fel.

Ha például hasonló tulajdonságokkal rendelkező típusok osztályait kell

elkészítenünk, akkor elvonatkoztatva az azokat megkülönböztető részletektől

egy általános osztályt kapunk. Az általánosítással összefogott osztályok

közös adattagjai az általános osztály adattagjai lesznek. Ugyanez a helyzet a

teljesen azonos metódusokkal: elég őket egyszer az általános osztályban

megadni.

Ha a konkrét osztályokat ebből az általános osztályból, mint

ősosztályból származtatjuk, akkor azok rendelkezni fognak az általános

osztályban definiált adattagokkal és metódusokkal, de ezen kívül egyéb

tulajdonságokkal is felruházhatók, azaz specializálhatók. Más szóval

kiegészíthetők további adattagokkal és metódusokkal. (A specializálás

fogalmán ugyan túlmutat, de elérhetjük azt is, hogy bizonyos adattagok és

metódusok ne öröklődjenek az általános osztályból.)

A specializáció következménye, hogy egy utódosztálybeli objektum

mindig elfogadható az ősosztályának objektumaként; azaz egy ősosztály

típusú változónak értékül lehet adni annak utódosztályához létrehozott

objektumot.

A specializálás során lehetőségünk van egy öröklött metódus

működésén változtatni. Így olyan osztályt származtathatunk, amely

rendelkezik ugyan az ősosztály egy bizonyos metódusával, annak neve,

Page 614: PROGRAMOZÁSEz 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

614

paramétereinek és visszatérésének típusa, azaz a deklarációja megegyezik az

ősosztálybelivel, de a működése eltér attól. Ehhez az utódosztályban az

öröklött metódust (pontosabban annak törzsét) kell felüldefiniálni vagy

újradefiniálni. E két fogalom nem szinonimája egymásnak, lényeges

különbség van közöttük, amely akkor mutatkozik meg, amikor egy ősosztály

típusú változónak értékül adjuk annak utódosztályához létrehozott

objektumot és meghívjuk rá a módosított metódust.

Az újradefiniált metódusnak csak annyi kapcsolata van az ősosztálybeli

megfelelőjével, hogy a metódus feje ugyanaz. Az újnak, amelyik felülírja a

régit, nincs semmi köze a régihez. Ha egy ősosztály típusú változónak értékül

adjuk annak utódosztályához létrehozott objektumot és meghívjuk rá ezt a

metódust, akkor az ősosztálybeli változat fog végrehajtódni, mert semmi

nem indokolja, hogy az ősosztálybeli változó számára látható metódus

helyett egy másik (azonos nevű és típusú) metódus hívódjon meg. Ezt a

jelenséget hívják statikus kötésnek. Az elnevezés arra utal, hogy már

fordítási időben eldől, hogy a változóhoz rendelt (kötött) metódus melyik az

azonos nevű és típusú metódusok közül.

A felüldefiniált metódus (amelyet virtuális metódusként is szokás

emlegetni) ellenben szoros kapcsolatban marad az eredeti, az ősosztálybeli

megfelelőjével. Az eredeti metódus ismeri önmaga felüldefiniált változatait,

ezért ha egy ősosztály típusú változóra hívják meg, akkor mindig az a véltozat

fog meghívódni, amelyik a változónak értékül adott objektumra (ez lehet egy

utódosztály objektuma) érvényes. Ezt a jelenséget hívják polimorfizmusnak

(többalakúság) vagy dinamikus kötésnek. A második elnevezés arra utal,

hogy csak futási időben, azaz dinamikus módon lehet el dönteni, hogy egy

ősosztálybeli változó az adott pillanatban milyen osztályú objektumot tárol,

és csak ennek ismeretében derül ki, hogy a kódban a változóhoz rendelt

(kötött) metódushívás valójában melyik metódus-változat működését váltja

ki. A kód-újrafelhasználás megvalósításához a dinamikus kötés, azaz a

felüldefiniálás igen erős eszközt ad a kezünkbe.

Az általánosítás során, amikor több olyan osztálynak készítjük el az

ősosztályát, amelyek rendelkeznek egy azonos fejű (nevű és típusú)

metódussal, az alábbiak szerint járunk el.

Page 615: PROGRAMOZÁSEz 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

615

1. Ha a vizsgált metódusok mindegyike ugyanazt csinálja az

utódosztályokban, akkor ezeket az utódosztályban nem definiáljuk,

elég egyetlen egyszer megadni ezt az ősosztályban, amelyet az

utódosztályok örökölnek, felüldefiniálni nem kell.

2. Ha a vizsgált metódusok az egyes utódosztályokban eltérően

működnek, akkor definiáljuk egyrészt az ősosztályban egy ott adekvát

működéssel, de az utódosztályokban külön-külön felüldefiniáljuk. (Ha

nincs szükség arra, hogy az ősosztályhoz létrehozzunk objektumokat,

akkor elég a metódusnak az ősosztályban csak a deklarációját

megadni. A metódus ilyenkor az ősosztályban absztrakt lesz.)

3. Ha a vizsgált metódusok működése tartalmaz közös részeket, akkor

ezeket a metódusokat a konkrét osztályokban ne definiáljuk, az

ősosztályban pedig úgy, hogy azokon a helyeken, ahol az eltérések

vannak egy-egy olyan ősosztálybeli metódust hívjunk meg, amellyeket

a származtatott osztályokban a kívánt módon felüldefiniálunk.

A származtatás lehet közvetett illetve többszörös. Közvetett

származtatásról akkor beszélünk, amikor a C osztály közvetlenül a B osztály

leszármazottja, a B pedig az A osztályé, és ennél fogva a C osztály közvetve az

A osztályból származik. A többszörös származtatás fogalma arra utal, hogy

egy osztálynak egyszerre több közvetlen őse is lehet.

Van úgy, hogy az általános osztályhoz is létrehozunk objektumot

(ilyenkor természetesen minden metódusát definiálni kell), de sokszor az

általános osztály csak arra szolgál, hogy abból más osztályokat

származtassunk, és soha nem hozunk létre belőle objektumokat. Ilyenor nem

kell azokat a metódusait definiálni, amelyeket a leszármazott osztályok úgyis

definiálnak, elegendő csak deklarálni ezeket. Az ilyen absztrakt metódusokat

tartalmazó általános osztályt absztrakt osztálynak hívjuk.2

2 Megjegyezzük, hogy az absztrakt osztály és az absztrakt típus fogalma

között nincs szoros összefüggés: az absztrakt osztály nem az absztrakt típus

megvalósítása. Az absztrakt típust egy adat jellemzésére szolgáló

fogalomként vezettük be, amelyet részben vagy nem végleges formában

valósítottunk meg. Az absztrakt osztály viszont a kód-újrafelhasználás

Page 616: PROGRAMOZÁSEz 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

616

Az általánosítás és specializálás jól alkalmazható az alternatív típus

megvalósításánál. (Ez az a típusszerkezet, amelyre a programozási nyelvek

általában nem biztosítanak közvetlen nyelvi eszközt ellentétben a rekord-

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

paramé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 sablon-

paraméterek megadása mellett új osztályt hozunk létre.

Page 617: PROGRAMOZÁSEz 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

617

hogy mindkét technikát egyszerre alkalmazzuk, de az is, hogy ugyanaz a

konkrét információ alternatív módon mindkét technikával hozzáadható egy

általános osztályhoz. Ilyenkor érdemes inkább a sablont használni, mivel a

sablon-példányosítás fordítási időben hajtódik végre.

Page 618: PROGRAMOZÁSEz 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

618

Nyelvi elemek

A fenti implementációs elveket nyelvi szinten a származtatás és a

sablonosítás támogatja.

Származtatásnál egy utódosztály definíciójában, a definíció fejében

kell jelölni a közvetlen ősosztályt. (Például C++ és C# nyelven erre a

kettőspont, Java nyelven az extends kulcsszó szolgál).

C++ nyelven egy utódosztályhoz egyszerre több közvetlen ősosztály is

megadható. A tisztán objektum-orientált nyelvekben a többszörös öröklődés

csak azzal a feltétellel lehetséges, hogy az ősosztályoknak egy kivételével,

úgynevezett interfészeknek kell lenniük. Az objektum-orientált nyelvekben

egy speciális kulcsszóval (interface) jelzett teljesen absztrakt osztályokat

hívják interfésznek. Járjuk körbe ezt a meghatározást. Implementációs

szempontból absztrakt osztály az, amelyhez nem akarunk létrehozni

objektumokat, nyelvi szempontból viszont az, amelyhez nem is tudunk. Ezt

megakadályozhatja például egy speciális kulcsszóval történő megjelölés (pl. a

C#, Java nyelveken ez a kulcsszó az abstract), de absztrakt osztály az is,

amelynek hiányos definíciója, azaz valamelyik (akár az összes) metódusának

a törzse hiányzik, más szóval vannak absztrakt metódusai. Absztrakttá lehet

tenni egy osztályt úgy is, hogy csak privát konstruktorai vannak. Teljesen

absztrakt osztály (tehát interfész) az, amelynek minden metódusa absztrakt

és nincsenek adattagjai. Teljesen absztrakt osztályokat a C++ nyelven is

készíthetünk, de nincs külön kulcsszó sem az absztrakt osztály, sem az

interfész jelölésére. Egy interfészből történő származtatáskor az interfész

összes metódusát felül kell definiálni. Erre mondjuk azt, hogy

implementáljuk az interfészt.

Az utódosztályra az ősosztály privát adattagjai és metódusai nem

öröklődnek. A védett (protected) minősítésű tagok az adott osztályra

nézve viszont ugyanúgy viselkednek, mint a privátok, ugyanakkor öröklődnek

az utódosztályra. Ezért érdemes a privát minősítéseket egy osztályban inkább

védettre cserélni, hacsak kifejezetten nem az a szándékunk, hogy egy

esetleges származtatásnál az adott tagot ne lehessen örökíteni.

C++ nyelven a származtatásnak többféle módja van. Ezek közül mi a

publikus származtatást fogjuk használni – ezt explicit módon jelölni kell –

amikor az ősosztály tagjainak láthatósága (public, protected) is öröklődik,

Page 619: PROGRAMOZÁSEz 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

619

azaz megmarad az utódosztályban. A tisztán objektum-orientált nyelvekben

többnyire csak ilyen publikus származtatással találkozhatunk.

Csak a tisztán objektum-orientált nyelvekben van lehetőség arra, hogy

bizonyos osztályokra megtiltsuk, hogy azokból más osztályokat

származtassunk. Az ilyen, a származtatási láncok legalján szereplő,

úgynevezett végső osztályokat speciális kulcsszavak jelzik (sealed, final).

Azokat a metódusokat, amelyeknek megengedjük a felüldefiniálásukat

virtuálisként (virtual) kell megjelölni. Bizonyos nyelvekben a

felüldefiniálás (override) vagy újradefiniálás (new) tényét külön is jelölni

kell az utódosztályban, de C++ nyelven erre nincs lehetőség. Ha egy

ősosztálybeli metódus virtuális, akkor csak felüldefiniálni lehet és ilyenkor a

leszármazott metódus is virtuális lesz. Ha az ősosztálybeli metódus nem

virtuális, akkor az utódosztályban csak az újradefiniálásáról lehet beszélni.

Általában az ősosztály metódusait (destruktorát is) a konstruktorai

kivételével virtuálisként adjuk meg.

A virtuális metódusok teremtik meg a polimorfizmus jelenségét. Nyelvi

szempontból egy ilyen metódus (ne felejtsük, hogy a virtuális metódus

felüldefiniáltja is virtuális) meghívásakor nem fordítási időben kötődik

(dinamikus kötés) a hívó utasításhoz a hívott metódus kódja. Az ugyanis,

hogy annak az objektumnak, amire a metódust meghívtuk mi a típusa, azaz

melyik osztálynak (az ősosztálynak vagy annak egy utódosztályának)

példánya, sokszor csak futás közben deríthető ki.

A polimorfizmus jelentősége akkor mutatkozik meg, amikor

programunkban egy ősosztály típusú változónak értékül adjuk egy

utódosztály objektumát. Ha egy ilyen változóra meghívunk egy virtuális

metódust, akkor a tisztán objektum-orientált nyelvekben az utódosztály

metódusa hajtódik végre. C++ nyelven ilyen helyzet úgy teremthető, ha egy

ősosztály típusú pointerváltozónak adjuk az utódosztálya egy példányának

címét, és erre a pointerváltozóra kezdeményezzük egy virtuális metódus

hívását. A C++ nyelven arra is lehetőség van, hogy egy ősosztály típusú

változónak (nem pointerváltozónak) közvetlenül adjuk értékül az utódosztály

egy objektumát (azaz látszólag ugyanazt tesszük, mint a tisztán objektum-

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

Page 620: PROGRAMOZÁSEz 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

620

lefutni. (A látszat ellenére a tisztán objektum-orientált nyelvek és a C++ nyelv

dinamikus kötése között valójában nincs különbség. A tisztán objektum-

orientá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ály-

sablonokkal 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ény-

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

Page 621: PROGRAMOZÁSEz 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

621

értékeket helyettesítő paraméternél az érték konkrét típusa. Ezt a template

< … > kifejezést meg kell ismételni az osztályon kívül (tehát nem inline

módon) definiált metódusok feje előtt is. A fejrészben a metódus neve előtt

nemcsak az osztály-sablon neve kell, hogy álljon minősítésként, hanem a név

után kisebb-nagyobb (< >) jelek között az összes paramétert is fel kell sorolni

azok típusának megjelölése nélkül. Tulajdonképpen ez az osztály-sablon

hivatalos neve. Ha egy metódus visszatérési típusa a saját osztály-sablonja

(ilyennel találkozhatunk a másoló konstruktornál), akkor itt is az előbb

említett teljes névvel (osztálynév+paraméterek) kell az osztály-sablonra

hivatkozni, ellenben a definíció többi részén elég az osztály-sablonnak csak

nevét használni a paraméterek felsorolása nélkül.

Példányosításkor a kódrészlet azonosítója (osztály-sablon esetén az

osztály neve) után kisebb-nagyobb jelek között kell a sablon-paraméterek

konkrét értékeit (típusokat illetve konstansokat) felsorolni ugyanabban a

sorrendben, mint ahogy azok a definícióban szerepeltek. Az utolsó néhány

paraméter rendelkezhet alapértelmezett értékkel, ezeket a példányosításnál

nem kell megadni.

Egy osztály-sablont is elhelyezhetünk külön állományban, de ez nem

lehet forrás állomány, ugyanis a sablon önmagában nem fordítható le csak a

példányosítása után. C++ nyelven ezért egy osztály-sablonnak mind az osztály

definícióját, mind a metódusainak törzsét egy közös fejállományba kell

helyezni, amelyet majd bemásolunk oda, ahol fel akarjuk használni. Nem

kötelező, de ajánlott ennek a fejállománynak a kiterjesztését .hpp-ként írni,

mert ezzel felhívjuk a figyelmet arra, hogy lényegében összevontuk azt, amit

az osztályok leírásánál külön .h és .cpp állományokba szoktunk tenni,

hiszen itt egy példányosítandó kódrész (sablon) található.

Page 622: PROGRAMOZÁSEz 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

622

35. Feladat: Túlélési verseny

Különféle élőlények ugyanazon a váltakozó terepekből álló pályán indulnak el

sorban egymás után. Egy lénynek attól függően változik az életereje, hogy

milyen terepen megy át, de közben a terepet is átalakítja. Egészen addig

halad, amíg végig nem ér a pályán vagy el nem fogy az életereje és elpusztul.

Egy terep akkor is átalakul, ha azon a lény elpusztul. Az első lény az eredeti

pályát, a további lények az elöttük levő által átalakított pályát használják.

Adjuk meg a pályán végig jutó, azaz életben maradt lények neveit!

A pályán három féle terep fordulhat elő: fű, homok, mocsár. A lények

három különböző fajta egyikéhez tartozhatnak.

Zöldike: kezdeti életereje 10; füvön az életereje eggyel nő, homokon

kettővel csökken, mocsárban eggyel csökken; a mocsaras terepet fűvé

alakítja, a másik két féle terepet nem változtatja meg.

Buckabogár: kezdeti életereje 15; füvön az ereje kettővel csökken,

homokon hárommal nő, mocsárban néggyel csökken; a füvet homokká, a

mocsarat fűvé alakítja, de a homokot nem változtatja meg.

Tocsogó: kezdeti életereje 20; füvön az életerő kettővel, homokon öttel

csökken, mocsárban hattal nő; a füvet mocsárrá alakítja, a másik két féle

terepet nem változtatja meg.

Minden lénynek van egy neve (sztring), ismert az aktuális életereje

(egész szám) és a fajtája. Egy lény addig él, amíg az életereje pozitív.

A verseny adatait egy szöveges állományból olvassuk be! A fájl első

sora tartalmazza a lények számát, amelyet a lények soronkénti leírása követ.

Ez a fajtát jelölő karakter (Z – zöldike, B – buckabogár, T – tocsogó), amit

szóköz után a lény neve követ. Ezek után következik a pálya leírása. Nem-

negatí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ó

Page 623: PROGRAMOZÁSEz 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

623

T szivacs

10 0 2 1 0 2 0 1 0 1 2

Page 624: PROGRAMOZÁSEz 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

624

Specifikáció

A feladat megoldásában központi szerepet játszanak a lények. Attól

függetlenül, hogy a lények konkrétan kicsodák vagy mi a fajtájuk, számos

közös tulajdonsággal rendelkeznek. Mindegyiknek van neve és életereje, meg

lehet róla kérdezni, hogy hívják (Név()), él-e (Él()) még, azaz az életereje

nagyobb-e nullánál, és szimulálni lehet a viselkedését a pálya egy bizonyos

terepén. Ez utóbbi művelet (Átalakít()) egyrészt módosítja a lény életerejét,

másrészt átalakítja a neki átadott terepet. Ennek a műveletnek a hatása attól

függ, hogy egy lény milyen fajtájú, ezért ez a művelet a lények általános

jellemzésének szintjén még nem implementálható. Ez nem baj, hiszen

általános lényeket úgysem akarunk létrehozni.

A lények leírásához bevezetünk négy osztályt. Az általános lény típusát

leíró osztály absztrakt lesz. Ebből származtatjuk a konkrét fajtájú lények,

zöldikék, buckabogarak és tocsogók osztályait. A származtatott osztályokban

felüldefiniáljuk az Átalakít() metódust.

14-1. ábra. Lények osztálydiagrammja

Page 625: PROGRAMOZÁSEz 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

625

Zöldikék esetében a kezdő életerő: 10, amit a konstruktor állít be. Az

Átalakít() művelet hatását az alábbi táblázat foglalja össze. Ez megkapja

bemenetként az aktuális terepet, és a táblázat megfelelő sora alapján

megváltoztatja az aktuális lény életerejét és visszaadja az új terepet.

terep életerő változás terepváltozás

homok -2 -

fű +1 -

mocsár -1 fű

Buckabogarak esetében a kezdő életerő: 15, és az Átalakít() művelet

hatása:

terep életerő változás terepváltozás

homok +3 -

fű -2 homok

mocsár -4 fű

Tocsogók esetében a kezdő életerő: 20, és az Átalakít() művelet

hatása:

terep életerő változás terepváltozás

homok -5 -

fű -2 mocsár

mocsár +6 -

Page 626: PROGRAMOZÁSEz 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

626

A lények absztrakt osztályát és az abból származtatott speciális lények

osztályait ezek alapján már könnyen elkészíthetjük. A speciális osztályok

konstruktorai meghívják az ősosztály konstruktorát, majd inicializálják az

életerőt. Az Él() és Név() metódusok az ősosztály szintjén implementálhatók.

Az Él() metódus akkor ad igaz értéket, ha az életerő pozitív. A Név() metódus

a név adattag értékét adja vissza. Az Átalakít() metódus az ősosztályban

absztrakt, a konkrét osztályok szintjén kell definiálni a korábban definiált

táblázatok alapján. Ez módosítja az életerőt és megváltoztatja az adott

pályamezőt.

Most pedig specifikálhatjuk a teljes feladatot. A specifikációban meg

kell különböztetni egy-egy lény áthaladta utáni pálya állapotokat. A nulladik

változat a kezdőpálya, az i-edik az i-edik lény mozgása után kialakult pálya.

A = ( kezdőpálya : ℕm

, lények : Lényn, 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:(ℕm

)n pálya[0]=kezdőpálya

i[1..n]: pálya[i]=Eredm(lények[i],pálya[i-1])2

névilényektúlélőkn

ÉlipályailényekEredmi

].[

().])[],[( 111

)

ahol az Eredm: Lény × ℕm Lény × ℕ

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 × ℕm

r(0) = (lény,pálya)

j[1..m]:

().)()(

().)())(()(

Éljrhajr

ÉljrhajrLépésjr

1

1

11

11

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

Page 627: PROGRAMOZÁSEz 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

627

r(j)2 pályát (valójában a pályának csak a j-edik mezője változhat a pálya előző

r(j-1)2 állapotához képest). Ezeket a változásokat a korábban definiált

Átalakít() függvény alapján számíthatjuk ki.

Ez így egy kicsit bonyolultnak tűnik, de objektum orientált szemlélettel

megfogalmazva a rekurzív függvény j-edik lépését az i-edik lényre már sokkal

egyszerűbb felírni: lények[i].Átalakít(pálya[j]), feltéve, ha lények[i].Él().

Absztrakt program

A megoldó programnak két szintje van. A felső szinten a fenti specifikációnak

megfelelő algoritmust írjuk le, az alsó szinten a lények osztályait.

A külső ciklus a lényeket veszi sorra. Minden lényt végig vezet a

pályán, de csak addig, amíg él, közben átalakítja a pályát, és ha túléli a lény a

pályát, akkor kiírja a nevét.

i = 1..n

j:=1

jm 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őklények[i].Név() SKIP

Page 628: PROGRAMOZÁSEz 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

628

Implementálás

A programkód a kommenteket nem számítva most is angol nyelvű lesz, de itt

talán célszerű egy kis magyar-angol szótárt is mellékelni a megvalósításhoz.

lény creature átalakít transmute

zöldike greenfinch pálya field

buckabogár sandbug terep ground

tocsogó squelchy fű grass

erő power homok sand

név name mocsár swamp

él alive

A program komponens szerkezete

Az absztrakt algoritmust a main.cpp állományban elhelyezett main

függvényben találjuk. Az osztályok definíciói a creature.h fejállományba,

az Transmute() metódusok implementációi a creature.cpp

forrásállományba kerülnek.

Főprogram kódolása

A main függvény az absztrakt program kódján kívül a lények és a pálya

beolvasását is tartalmazza.

A versenyen résztvevő lényekre történő hivatkozásokat, azaz

Creature* típusú elemeket egy tömbben (vector<Creature*>) tároljuk.

Ha nem a Creature osztály egy objektumát, hanem a Creature osztályból

származtatott osztály egy objektumát hozzuk létre, akkor ennek hivatkozása

(címe) is elhelyezhető ebben a tömbben. Így lényegében egy olyan tömböt

használunk, amelyik vegyesen tárolhat különböző, de a Creature osztályból

Page 629: PROGRAMOZÁSEz 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

629

származtatott osztályú (típusú) elemeket: a tömb egy elemének tehát

alternatív szerkezetű típusa van.

ifstream f("input.txt");

int n;

f >> n;

vector<Creature*> creatures(n);

for(int i=0; i<n; ++i){

char l;

string a;

f >> l >> a;

switch(l){

case 'T' : creatures[i] = new Squelchy(a);

break;

case 'Z' : creatures[i] = new Greenfinch(a);

break;

case 'B' : creatures[i] = new Sandbug(a);

break;

default:;

}

}

Page 630: PROGRAMOZÁSEz 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

630

A pálya a pályamezők számkódjait tartalmazó tömbbe (vector<int>)

kerül.

int m;

f >> m;

vector<int> field(m);

for(int j=0; j<m; ++j) f >> palya[j];

A feldolgozást a struktogramm alapján kódoljuk. Figyelembe kell venni,

hogy a creatures tömb a C++ megvalósításban pointereket tárol, ezért

például az i-edik lény által okozott átalakítást itt a creatures[i]-

>Transmute() alakú metódushívással tehetjük meg.

for(int i=0; i<n; ++i){

for(int j=0; creatures[i]->Alive() && j<m; ++j){

creatures[i]->Transmute(palya[j]);

}

if (creatures[i]->Alive())

cout << creatures[i]->Name() << endl;

}

A program végén ne felejtsük el felszabadítani a saját

memóriafoglalásainkat.

for(int i=0; i<n; ++i){

delete creatures[i];

Page 631: PROGRAMOZÁSEz 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

631

}

Creature osztály

Ez a specifikációnak megfelelő absztrakt osztály. Absztrakt voltára két dolog

is felhívja a figyelmet: a konstruktora nem publikus és a Transmute()

metódusa nincs implementálva. Ennél fogva ilyen típusú objektumot nem

lehet létrehozni.

class Creature {

protected:

std::string name;

int power;

Creature(std::string a):name(a) {}

public:

std::string Name() const { return name;}

bool Alive() const { return power > 0;}

virtual void Transmute(int &gound) = 0;

virtual ~Creature(){}

};

Fontos, hogy az Transmute() metódus virtuális legyen, hiszen ez jelzi

a fordítónak, hogy egy ilyen metódus hívását nem szabad fordítási időben

kiértékelni, majd csak futás közben dől el, hogy a származtatott osztályok

közül melyiknek a Transmute() metódusa fut le (dinamikus kötés). Ez

pedig attól függ majd, hogy valójában milyen típusú objektumra mutat az a

hivatkozás, amellyel ezt metódust meghívjuk.

Page 632: PROGRAMOZÁSEz 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

632

Speciális lények osztályai

A specifikáció meghatározta a Creature osztály leszármazott osztályait is.

Ezek rendelkeznek az ősosztály védett tagjaival, csak konstruktort kell

megadniuk és a Transmute() metódust felüldefiniálniuk.

A konstruktorok az adott fajtájú lényre jellemző kezdeti életerőt

állítják be azt követően, hogy az ősosztály konstruktorát meghívva beállítják

az objektum (konkrét lény) nevét is. Ez a név a konstruktor bemenő

paramétere.

class Greenfinch : public Creature {

public:

Greenfinch(std::string a):Creature(a){power=10;}

void Transmute(int &gound);

};

class Sandbug : public Creature {

public:

Sandbug(std::string a):Creature(a){power = 15;}

void Transmute(int &gound);

};

class Squelchy : public Creature {

public:

Squelchy(std::string a):Creature(a){power = 20;}

void Transmute(int &gound);

};

Page 633: PROGRAMOZÁSEz 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

633

A Transmute() metódus deklarációját az utódosztályok megismétlik,

majd a specifikációban megadott táblázat alapján háromféleképpen

definiálják.

void Greenfinch::Transmute(int &gound)

{

switch(gound){

case 1: power+=1; break;

case 0: power-=2; break;

case 2: power-=1; gound = 1; break;

default:;

}

}

void Sandbug::Transmute(int &gound)

{

switch(gound){

case 1: power-=2; gound = 0; break;

case 0: power+=3; break;

case 2: power-=4; gound = 1; break;

default:;

}

}

Page 634: PROGRAMOZÁSEz 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

634

void Squelchy::Transmute(int &gound)

{

switch(gound){

case 1: power-=2; gound = 2; break;

case 0: power-=5; break;

case 2: power+=6; break;

default:;

}

}

Ez mindhárom esetben egy elágazás, amely a bemenetként megadott

tereptől függően módosít a lény életerején és megváltoztatja, ha kell, a

terepet. Feltételezzük, hogy ezek a metódusok csak akkor kerülnek

meghívásra, amikor a lény még él, ezért ezt itt külön nem ellenőrizzük.

Tesztelés

Fekete doboz tesztesetek:

Érvényes adatok:

1. Nincsenek lények.

2. Nulla hosszúságú a pálya (minden lény életben marad).

3. Az első illetve az utolsó mezőre lépve fogy el egy lény életereje.

4. Egy speciális lény kipróbálása (mindhárom fajtára külön-külön) olyan

pályán, ahol egymás után mindhárom talaj előfordul, és ezeken a

lény végig megy (életben marad). Ehhez a teszthez érdemes kiíratni a

megváltozott pályát, hogy a változásokat számszerűen is láthassuk.

5. Egy speciális lény kipróbálása (mindhárom fajtára külön-külön) olyan

pályán ahol a lény életereje elfogy.

6. Általános eset sok lénnyel.

Page 635: PROGRAMOZÁSEz 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

635

Érvénytelen adatokra nincs felkészítve a fenti program. Nem létező állomány

vagy hibás formátumú állomány esetén a program elromlik.

Fehér doboz tesztesetek: A fenti esetek tesztelik a program minden

utasítását. Dinamikus helyfoglalások miatt viszont tesztelni kellene még a

memória szivárgást.

Komponens tesztre külön nincs szükség.

Page 636: PROGRAMOZÁSEz 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

636

Teljes program

main.cpp:

#include <iostream>

#include <fstream>

#include <vector>

#include "creature.h"

using namespace std;

int main()

{

ifstream f("input.txt");

int n;

f >> n;

vector<Creature*> creatures(n);

for(int i=0; i<n; ++i){

char l;

string a;

f >> l >> a;

switch(l){

case 'T' : creatures[i] = new Squelchy(a);

break;

case 'Z' : creatures[i] = new Greenfinch(a);

Page 637: PROGRAMOZÁSEz 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

637

break;

case 'B' : creatures[i] = new Sandbug(a);

break;

default:;

}

}

int m;

f >> m;

vector<int> palya(m);

for(int j=0; j<m; ++j) f >> palya[j];

for(int i=0; i<n; ++i){

for(int j=0; creatures[i]->Alive() && j<m;

++j){

creatures[i]->Transmute(palya[j]);

}

if (creatures[i]->Alive())

cout << creatures[i]->Name() << endl;

}

for(int i=0; i<n; ++i) delete creatures[i];

return 0;

}

Page 638: PROGRAMOZÁSEz 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

638

creature.h:

#ifndef CREATURE_H

#define CREATURE_H

#include <string>

class Creature {

protected:

std::string name;

int power;

Creature(std::string a):name(a) {}

public:

std::string Name() const { return name;}

bool Alive() const { return power > 0;}

virtual void Transmute(int &gound) = 0;

virtual ~Creature(){}

};

class Greenfinch : public Creature {

public:

Greenfinch(std::string a):Creature(a){power=10;}

void Transmute(int &gound);

};

Page 639: PROGRAMOZÁSEz 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

639

class Sandbug : public Creature {

public:

Sandbug(std::string a):Creature(a){power = 15;}

void Transmute(int &gound);

};

class Squelchy : public Creature {

public:

Squelchy(std::string a):Creature(a){power = 20;}

void Transmute(int &gound);

};

#endif

Page 640: PROGRAMOZÁSEz 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

640

creature.cpp:

#include "creature.h"

using namespace std;

void Greenfinch::Transmute(int &gound)

{

switch(gound){

case 1: power+=1; break;

case 0: power-=2; break;

case 2: power-=1; gound = 1; break;

default:;

}

}

void Sandbug::Transmute(int &gound)

{

switch(gound){

case 1: power-=2; gound = 0; break;

case 0: power+=3; break;

case 2: power-=4; gound = 1; break;

default:;

}

}

Page 641: PROGRAMOZÁSEz 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

641

void Squelchy::Transmute(int &gound)

{

switch(gound){

case 1: power-=2; gound = 2; break;

case 0: power-=5; break;

case 2: power+=6; break;

default:;

}

}

Page 642: PROGRAMOZÁSEz 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

642

36. Feladat: Lengyel forma és kiértékelése

Alakítsunk át egy infix formájú, egész számokból, alapműveleti jelekből és

zárójelekből álló aritmetikai kifejezést postfix (lengyel) formájúra, és

számoljuk ki az értékét.

Specifikáció

A feladat specifikációja önmagában nem ad túl sokat árul el a

lehetséges megoldásról.

A = ( a : String*, z : ℤ )

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.

Page 643: PROGRAMOZÁSEz 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

643

A = ( y : Token*, 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

Page 644: PROGRAMOZÁSEz 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

644

Definiáljuk a tokenek általános osztályát (absztrakt ősosztály) és ebből

származtatva az egyes tokenfajták konkrét osztályait. A fajta lekérdezését

biztosító Is_ kezdetű metódusok ősosztálybeli definíciójuk szerint hamis

értéket adnak vissza, de a megfelelő osztályokbeli felüldefiniálásuk az igaz

értéket. Így egy konkrét tokenre mindig pontosan az egyik Is_ kezdetű

metódus ad csak igazat, éppen az, amilyen a token fajtája.

Az operandus tokeneknek lekérdezhető az értékük. Az operátor

tokeneknek megkérdezhetjük a prioritását (a szorzás és osztás magasabb

prioritású az összeadásnál és kivonásnál), és kiszámolhatjuk két egész

számnak az adott oparátorral elvégzett eredményét.

A részfeladatok megoldásánál szükség lesz egy olyan gyűjteményre,

amelyben token-sorozatot tudunk tárolni. Egy ilyen sorozatot először fel kell

tölteni, mondjuk a sorozat végéhez történő hozzáfűzés műveletével, majd be

kell járni az elemeit. A 34. feladatban definiáltuk a BiQueue kettős sorok

osztályát, amely rendelkezik egy sorozat végére író (Hiex()t) művelettel és

bejárót (Enumerator) is lehetett hozzá készíteni. Sajnos azonban az a

BiQueue típusó sorozat csak egész számok tárolására alkalmas. De ha

elkészítjük a BiQueue osztály olyan sablonját, amely olyan sorozatokat

definiál, amelyek elemeinek típusát egy sablon-paraméter helyettesíti, akkor

ezt már felhasználhatjuk akár tokenek sorozatának tárolására.

Egy szintaktikailag helyes infix forámjú aritmetikai kifejezés postfix

formájúra alakításának algortimusa jól ismert.

x.First() y:=<>

x.End()

t: = x.Current()

t.Is_Operand() t.Is_LeftP() t.Is_RightP() t.Is_Operator()

y:Hiext(t)

s.Push(t)

s.Top().Is_Left()

s.Empty()

s.Top().Is_Left()

s.Top().Priority()

Page 645: PROGRAMOZÁSEz 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

645

>t.Priority()

y:Hiext(s.Pop()) y:Hiext(s.Pop())

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

Page 646: PROGRAMOZÁSEz 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

646

értékeit, amelyek itt egész számok. Ha elkészítjük a 33. feladatban szereplő

Stack osztálynak olyan egy sablonját, amellyel olyan vermek írhatók le, ahol

az elemek típusát egy sablon-paraméter helyettesíti, akkor ezt

felhasználhatjuk mind tokeneket tároló verem, mind egész számokat tároló

verem létrehozásához.

Implementálás

A program komponens szerkezete

A program több részből áll. A stack.hpp állományban a verem

osztály-sablonját, a biqueue.hpp állományban helyezzük el a kettős sor

osztály-sablonját, a token.h és token.cpp állományok tartalmazzák a

tokenek ősosztályát és a tokenek fajtáinak az ősosztályból származtatott

osztályait. A main.cpp állomány main függvényben találjuk a feldolgozás

három lépését.

Tokenek osztályai

A Token osztály, valamint az abból származtatott osztályok definícióit

a terv alapján készítjük el.

class Token{

friend std::istream& operator>>(std::istream&,

Token*&);

public:

class IllegalElementException{

private:

char ch;

public:

IllegalElementException(char c) : ch(c){}

Page 647: PROGRAMOZÁSEz 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

647

char Message() const { return ch;}

};

virtual ~Token(){}

virtual bool Is_LeftP() const {return false;}

virtual bool Is_RightP() 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

Page 648: PROGRAMOZÁSEz 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

648

fontos eleme a megoldásunknak, ugyanis ezzel az operátorral tudjuk egy

tetszőleges adatfolyam karaktersorozatából beolvasni a soron következő

tokennek megfelelő karaktereket és magát a tokent létrehozni.

istream& operator >> (istream& s, Token* &t) {

char ch;

s >> ch;

switch(ch){

case '0' : case '1' : case '2' : case '3' :

case '4' : case '5' : case '6' : case '7' :

case '8' : case '9' :

s.putback(ch);

int intval;

s >> intval;

t = new Operand(intval);

break;

case '+' : case '-' : case '*' : case '/':

t = new Operator(ch); break;

case '(' : t = new LeftP(); break;

case ')' : t = new RightP(); break;

case ';' : t = new End(); break;

default: if(!s.fail()) throw new

Token::IllegalElementException(ch);

}

return s;

Page 649: PROGRAMOZÁSEz 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

649

}

A Token osztályban definiáljuk azt a kivétel-osztályt, amelynek

példányait kivételként dobjuk a tokenizálás során, ha nem megfelelő

karakterrel találkozunk.

Tárolók osztály-sablonjai

A tárolók (vermek és a kettős sorok) osztály-sablonjainak

elkészítésekor a korábban már létrehozott Stack és BiQueue osztályokból

indulunk ki (lásd előző fejezet feladatait).

Ahhoz, hogy a Stack osztályból sablont készítsünk, meg kell

keresnünk a definíciójában az összes olyan részletet, ahol a verembeli

elemek típusára int-ként hivatkozunk. Ilyen például a Top() és a Pop()

visszatérési típusa, a Push() paraméterváltozójának típusa, a beágyazott

Node struktúra (amely automatikusan sablonná válik) val adattagjának és

konstruktora első paraméterváltozójának típusa, valamint a Pop() lokális e

változójának típusa. Ezeket mind kicsréljük az Item sablon-paraméterre.

Azoknál a metódusoknál, ahol a bemenő paraméterváltozó típusaként

szerepelt a lecserélendő int szó, ott ezt a const Item& típussal kell

helyettesíteni, hiszen a sablon-paraméter helyébe egy példányosításnál

összetett típus is kerülhet, és nem lenne szerencsés (memória pazarlás) ha a

metódus bemenő paraméterének értékét lemásolva adnánk át azt a

paraméterváltozójának. Természetesen el kell még helyezni a sablon

jelöléséhez szükséges nyelvi elemeket: az osztály és az azon kívül definiált

metódusai elé (template <typename Item>), a kívül definiált metódusok

neve előtt a Stack<Item>:: minősítést használjuk, továbbá a másoló

konstruktor visszatérési típusát is Stack<Item>-re kell cserélni

Hasonló tennivalónk van a BiQueue sablonosításánál. Ne feledkezzünk

meg a beágyazott Enumerator osztály Current() metódusának

visszatérési típusáról sem. A beágyazott osztály ugyanis ugyanúgy sablonná

válik, mint az beágyazó. Ügyeljünk arra, hogy ne automatikus cserét

végezzünk, mert vannak a kódban olyan int definíciók, amelyeket nem

Page 650: PROGRAMOZÁSEz 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

650

szabad Item-re cserélni (ilyen például az enumeratorCount adattag

típusa).

Mindkét sablont a feladat végén elhelyezett teljes kódban tekinthetjük

meg.

Főprogram

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

Page 651: PROGRAMOZÁSEz 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

651

<< ex->Message() << endl;

delete ex;

DeallocateToken(x);

exit(1);

}

A második szakasz a tervben megadott algoritmust kódolja. Az x kettős

sor elemeinek bejárásához egy felsorló objektumot hozunk létre. Token*

típussal példányosítjuk a Stack-et és a BiQueue-t, így definiáljuk a

megoldáshoz szükséges s vermet és az eredményt tartalmazó y kettős sort.

A kódba minden olyan ponton, hibaellenőrzést építünk be, amelyre

szintaktikusan hibás aritmetikai kifejezés esetén kerülne a vezérlés: ilyen a

verem idő előtti kiürülése vagy nem várt token előfordulása.

Jól megfigyelhető a kódban a polimorfizmus jelensége, nevezetesen

az, amikor a Token* típusú t változóra meghívjuk például az

Is_Operator()-t vagy valamelyik másik virtuális lekérdező metódust. Ezek

a hívó utasítások nem értelmezhetőek fordítási időben, mert a hatásuk attól

függ, hogy futási időben éppen milyen fajta tokenre mutató cím található a

t-ben. A t->Is_Operator() eredménye attól függően lesz igaz vagy

hamis, hogy a t egy operátorfajta tokenre mutat vagy sem.

Egészen más a helyzet ((Operator*)s.Top())->Priority()

hivatkozással. Az s.Top()->Priority() kifejezést a fordító önmagában

nem tudja értelmezni, hiszen a Token osztálynak nincs Priority()

metódusa. Ha viszont az s.Top() értékét Operator* típusú címmé

konvertáljuk (az öröklődési kapcsolat miatt ezt szabad), akkor – mivel az

Operator-nak van Priority() metódusa – a kifejezés már lefordítható. Ez

az úgynevezett statikus konverzió

(static_cast<Operator*>(s.Top())), amelyet a kódban az egyszerűbb

C nyelvi írásmóddal (((Operator*)s.Top())) jelölünk. Ez a megoldás

veszélyes lehet, ha a t változóba futás közben más fajtájú token címe is

kerülhetni, mont operátor. Itt azonban biztosak lehetünk abban, hogy amikor

ez a kifejezés kiértékelésre kerül, akkor az s verem tetején operátor fajtájú

Page 652: PROGRAMOZÁSEz 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

652

token van (lásd a kifejezést beágyazó ciklus feltételét). Ha ilyen

bizonyosságunk nem lenne és csak futási időben derülhetne ki, hogy egy

ilyen átalakítás helyes-e vagy sem, akkor az úgynevezett dinamikus

konverziót kellene alkalmazni ((dynamic_cast<Operator*>(s.Top()))).

A megoldás harmadik szakaszának kódja a tervben megadott második

algoritmust követi, de ebben is elhelyezünk néhány hibaellenőrzést.

Végül megjegyezzük, hogy a kódban sehol sem figyelünk a vermek

esetleges FULLSTACK kivételére.

Tesztelés

A BiQueue és a Stack tesztelését már korábban megtettük.

A feladat fekete doboz tesztelésének keretében az érvényes

tesztadatok a szintaktikusan helyes aritmetikai kifejezések lesznek. Ezek

között feltételnül meg kell vizsgálni a többszörösen zárójelezett

kifejezéseket, olyanokat, ahol különböző priorítású műveleti jelek különféle

sorrendben fordulnak elő egy zárójelezetlen részben. Érvénytelen adatok a

különféle szintaktikusan helytelen kifejezések.

A tesztesetek részletes kidolgozását az Olvasóra bízzuk.

Page 653: PROGRAMOZÁSEz 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

653

Teljes program

main.cpp:

#include <iostream>

#include <cstdlib>

#include „token.h”

#include „stack.hpp”

#include „biqueue.hpp”

using namespace std;

void DeallocateToken(BiQueue<Token*> &x);

int main()

{

cout << „Add meg az aritmetikai kifejezést!\n”;

cout << „Írj a végére pontosvesszőt!\n”;

// Tokenizálás

BiQueue<Token*> x;

try{

Token *t;

Page 654: PROGRAMOZÁSEz 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

654

cin >> t;

while(!t->Is_End()){

x.Hiext(t);

cin >> t;

}

}catch(Token::IllegalElementException *ex){

cout << „Illegális karakter: „

<< ex->Message() << endl;

delete ex; DeallocateToken(x); exit(1);

}

Page 655: PROGRAMOZÁSEz 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

655

// Lengyel formára hozás

BiQueue<Token*> y;

Stack<Token*> s;

BiQueue<Token*>::Enumerator itx

= x.CreateEnumerator();

for(itx.First(); !itx.End(); itx.Next()){

Token *t = itx.Current();

if(t->Is_Operand()) y.Hiext(t);

else if (t->Is_LeftP()) 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);

Page 656: PROGRAMOZÁSEz 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

656

}

}

}else if (t->Is_Operator()) {

while(!s.Empty()

&& s.Top()->Is_Operator()

&& ((Operator*)s.Top())->Priority()

> ((Operator*)t)->Priority() )

y.Hiext(s.Pop());

s.Push(t);

}else{

cout << „Szintaktikai hiba!” << endl;

DeallocateToken(x);

exit(1);

}

}

while(!s.Empty()){

if(s.Top()->Is_LeftP()){

cout << „Szintaktikai hiba!” << endl;

DeallocateToken(x);

exit(1);

}else y.Hiext(s.Pop());

}

Page 657: PROGRAMOZÁSEz 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

657

// Kiértékelés

try{

Stack<int> v;

BiQueue<Token*>::Enumerator ity

= y.CreateEnumerator();

for(ity.First(); !ity.End(); ity.Next()){

Token *t = ity.Current();

if (t->Is_Operand())

v.Push( ((Operand*)t)->Value() );

else

v.Push( ((Operator*)t)->

Evaluate(v.Pop(),v.Pop()) );

}

int r = v.Pop();

if(!v.Empty()) {

cout << „Szintaktikai hiba!” << endl;

DeallocateToken(x);

exit(1);

}

cout << „A kifejezes erteke: „ << r << endl;

}catch(Stack<int>::Exceptions ex){

if(Stack<int>::EMPTYSTACK == ex){

Page 658: PROGRAMOZÁSEz 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

658

cout << „Szintaktikai hiba!” << endl;

DeallocateToken(x);

exit(1);

}

}

DeallocateToken(x);

return 0;

}

void DeallocateToken(BiQueue<Token*> &x)

{

BiQueue<Token*>::Enumerator itx

= x.Createenumerator();

for(itx.First(); !itx.End(); itx.Next()){

delete itx.Current();

}

}

Page 659: PROGRAMOZÁSEz 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

659

token.h:

#ifndef TOKEN_H

#define TOKEN_H

#include <string>

#include <sstream>

class Token{

friend std::istream& operator>>(std::istream&,

Token*&);

public:

class IllegalElementException{

private:

char ch;

public:

IllegalElementException(char c) : ch®{}

char Message() const { return ch;}

};

virtual ~Token(){}

virtual bool Is_LeftP() const {return false;}

virtual bool Is_RightP() const {return false;}

virtual bool Is_Operand() const {return false;}

Page 660: PROGRAMOZÁSEz 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

660

virtual bool Is_Operator() const {return false;}

virtual bool Is_End() const {return false;}

};

class Operand: public Token{

public:

Operand(int v) {val=v;}

bool Is_Operand() const {return true; }

int Value() const {return val;}

protected:

int val;

};

Page 661: PROGRAMOZÁSEz 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

661

class Operator: public Token{

public:

Operator(char o) {op = o;};

bool Is_Operator() const {return true; }

int Priority() const;

int Evaluate(int a, int b) const;

protected:

char op;

};

class RightP: public Token{

public:

bool Is_RightP() const {return true; }

};

class LeftP: public Token{

public:

bool Is_LeftP() const {return true; }

};

class End: public Token{

public:

bool Is_End() const {return true; }

};

Page 662: PROGRAMOZÁSEz 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

662

#endif

Page 663: PROGRAMOZÁSEz 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

663

token.cpp:

#include „token.h”

#include <sstream>

#include „stack.hpp”

using namespace std;

istream& operator >> (istream& s, Token* &t) {

char ch;

s >> ch;

switch(ch){

case ’0’ : case ’1’ : case ’2’ : case ’3’ :

case ’4’ : case ’5’ : case ’6’ : case ’7’ :

case ’8’ : case ’9’ :

s.putback(ch);

int intval;

s >> intval;

t = new Operand(intval);

break;

case ’+’ : case ’-’ : case ’*’ : case ’/’:

t = new Operator(ch); break;

case ’(’ : t = new LeftP(); break;

case ’)’ : t = new RightP(); break;

case ’;’ : t = new End(); break;

default: if(!s.fail()) throw new

Page 664: PROGRAMOZÁSEz 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

664

Token::IllegalElementException(ch);

}

return s;

}

int Operator::Priority() const

{

switch(op){

case ’+’ : case ’-’ : return 1;

case ’*’ : case ’/’ : return 2;

default: return 3;

}

}

int Operator::Evaluate(int a, int b) const

{

switch(op){

case ’+’: return a+b;

case ’-’: return a-b;

case ’*’: return a*b;

case ’/’: return a/b;

default:;

}

return 0;

}

stack.hpp:

Page 665: PROGRAMOZÁSEz 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

665

#ifndef STACK_HPP

#define STACK_HPP

#include <iostream>

#include <memory>

template <typename Item>

class Stack{

public:

enum Exceptions{EMPTYSTACK, FULLSTACK};

Stack();

~Stack();

Stack(const Stack&);

Stack& operator=(const Stack&);

void Push(const Item &e);

Item Pop();

Item Top() const;

bool Empty() const;

private:

struct Node{

Item val;

Node *next;

Page 666: PROGRAMOZÁSEz 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

666

Node(const Item &e, Node *n)

: val®, next(n){}

};

Node *head;

};

template <typename Item>

Stack<Item>::Stack(): head(NULL){}

template <typename Item>

Stack<Item>::~Stack()

{

Node *p;

while(head != NULL){

p = head;

head = head->next;

delete p;

}

}

template <typename Item>

void Stack<Item>::Push(const Item &e)

{

Page 667: PROGRAMOZÁSEz 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

667

try{ head = new Node(e,head);}

catch(std::bad_alloc o){ throw FULLSTACK;}

}

template <typename Item>

Item Stack<Item>::Pop()

{

if(NULL == head) throw EMPTYSTACK;

Item e = head->val;

Node *p = head;

head = head->next;

delete p;

return e;

}

template <typename Item>

Item Stack<Item>::Top()const

{

if(NULL == head) throw EMPTYSTACK;

return head->val;

}

template <typename Item>

bool Stack<Item>::Empty()const

{

return NULL == head;

}

Page 668: PROGRAMOZÁSEz 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

668

template <typename Item>

Stack<Item>::Stack(const Stack& s)

{

if(NULL == s.head) head = NULL;

else {

try{

head = new Node(s.head->val,NULL);

}catch(std::bad_alloc o){

throw FULLSTACK;

}

Node *q = head;

Node *p = s.head->next;

while(p != NULL){

try{ q->next = new Node(p->val,NULL);}

catch(std::bad_alloc o){throw FULLSTACK;}

q = q->next;

p = p->next;

}

}

}

template <typename Item>

Stack<Item>& Stack<Item>::operator=(const Stack& s)

{

Page 669: PROGRAMOZÁSEz 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

669

if(&s == this) return *this;

Node *p;

while(head != NULL){

p = head;

head = head->next;

delete p;

}

if(NULL == s.head) head = NULL;

else {

try{ head = new Node(s.head->val,NULL);}

catch(std::bad_alloc o){throw FULLSTACK;}

Node *q = head;

Node *p = s.head->next;

while(p != NULL){

try{ q->next = new Node(p->val,NULL);}

catch(std::bad_alloc o){throw FULLSTACK;}

q = q->next;

p = p->next;

}

}

return *this;

Page 670: PROGRAMOZÁSEz 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

670

}

#endif

Page 671: PROGRAMOZÁSEz 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

671

biqueue.hpp:

#ifndef BIQUEUE_HPP

#define BIQUEUE_HPP

#include <memory>

template <typename Item>

class BiQueue{

public:

enum Exceptions{EMPTYSEQ, UNDERTRAVERSAL};

BiQueue():

first(NULL),last(NULL),enumeratorCount (0){}

BiQueue(const BiQueue&);

BiQueue& operator=(const BiQueue&);

~BiQueue();

void Loext(const Item &e);

Item Lopop();

void Hiext(const Item &e);

Item Hipop();

private:

struct Node{

Item val;

Node *next;

Node *prev;

Node(const Item &c, Node *n, Node *p)

Page 672: PROGRAMOZÁSEz 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

672

: val®, next(n), prev(p){};

};

Node *first;

Node *last;

int enumeratorCount;

public:

class Enumerator{

public:

Enumerator(BiQueue *p):bq(p),current(NULL)

{++(bq->enumeratorCount);}

~Enumerator(){--(bq->enumeratorCount);}

Item Current()const {return current->val;}

void First() {current = bq->first;}

bool End() const {return NULL == current;}

void Next() {current = current->next;}

private:

BiQueue *bq;

Node *current;

};

Enumerator CreateEnumerator()

{return Enumerator(this);}

};

template <typename Item>

BiQueue<Item>::~BiQueue(){

Page 673: PROGRAMOZÁSEz 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

673

Node *p, *q;

q = first;

while( q != NULL){

p = q;

q = q->next;

delete p;

}

}

template <typename Item>

BiQueue<Item>::BiQueue(const BiQueue &s){

if(NULL == s.first)first = last = NULL;

else{

Node *q = new Node(s.first->val,NULL,NULL);

first = q;

for(Node *p=s.first->next;p!=NULL;p=p->next){

q = new Node(p->val,NULL,q);

q->prev->next = q;

}

last = q;

}

}

template <typename Item>

Page 674: PROGRAMOZÁSEz 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

674

BiQueue<Item>& BiQueue<Item>::operator=(

const BiQueue &s){

if(&s == this) return *this;

Node *p = first;

while(p != NULL){

Node *q = p->next;

delete p;

p = q;

}

if(NULL == s.first) first = last = NULL;

else{

Node *q = new Node(s.first->val,NULL,NULL);

first = q;

for(Node *p=s.first->next;p!=NULL;p=p->next){

q = new Node(p->val,NULL,q);

q->prev->next = q;

}

last = q;

}

return *this;

}

template <typename Item>

void BiQueue<Item>::Loext(const Item &e){

Page 675: PROGRAMOZÁSEz 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

675

Node *p = new Node(e,first,NULL);

if(first != NULL) first->prev = p;

first = p;

if(NULL == last) last = p;

}

template <typename Item>

Item BiQueue<Item>::Lopop(){

if(enumeratorCount != 0) throw UNDERTRAVERSAL;

if(NULL == first) throw EMPTYSEQ;

int e = first->val;

Node *p = first;

first = first->next;

delete p;

if(first != NULL) first->prev = NULL;

else last = NULL;

return e;

}

template <typename Item>

void BiQueue<Item>::Hiext(const Item &e){

Node *p = new Node(e,NULL,last);

if(last != NULL) last->next = p;

last = p;

Page 676: PROGRAMOZÁSEz 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

676

if(NULL == first) first = p;

}

template <typename Item>

Item BiQueue<Item>::Hipop(){

if(enumeratorCount != 0) throw UNDERTRAVERSAL;

if(NULL == last)throw EMPTYSEQ;

int e = last->val;

Node *p = last;

last = last->prev;

delete p;

if(last != NULL) last->next = NULL;

else first = NULL;

return e;

}

#endif

Page 677: PROGRAMOZÁSEz 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

677

37. Feladat: Bináris fa bejárása

Készítsünk egy bináris fa-típust! A típusnak támogatnia kell a fa pre-, in- és

postorder bejárását! Egy bejárásnál paraméterként lehessen megadni azt a

tevékenységet, amit a bejáráskor az egyes csúcsokon kell majd végrehajtani!

Definiáljunk ilyen tevékenységeket a fa csúcsaiban tárolt értékek kiírására, a

csúcsbeli értékek összegzésére és a belső csúcsbeli értékek maximumának

meghatározására!

Specifikáció

A bináris fa típusának jellemző műveletei:

Preorder bejárás

Inorder bejárás

Postorder bejárás

Új érték új csúcsként való beillesztése

Eldönteni, hogy egy csúcs levélcsúcs-e

Eldönteni, hogy egy csúcs belső csúcs-e

A három bejárás mindegyikének paraméterként egy úgynevezett

tevékenység objektumot lehet majd átadni. A tevékenység objektum

rendelkezik egy „hajts vége” művelettel, amelyik bemenő adatként a

bejáráskor érintett csúcsot kapja meg. A bejárás végig adogatja a

tevékenység objektumot a bináris fa csúcsain, és minden csúcsra rendre

meghívja a tevékenység objektum „hajts vége” műveletét. Bizonyos

tevékenységek valamilyen eredményt számolnak a bejárt csúcsok értékeiből

(összeg, maximális érték stb.), ezért egy tevékenység objektum rendelkezhet

privát adattagokkal, amelyeket az objektum létrehozása inicializál, a „hajts

vége” művelete módosít, és ezek természetesen lekérdezhetőek.

A „fába új elemet beszúr” műveletet ismételt meghívásával leszünk

majd képesek egy bináris fát felépíteni. Ezt a műveletet most úgy valósítjuk

Page 678: PROGRAMOZÁSEz 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

678

majd meg, hogy az a fának egy véletlenszerűen kiválasztott ágának végére

függessze fel az új csúcsot.

Absztrakt program

A feladat megoldásához több egymáshoz szorosan kapcsolódó osztály-

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

Page 679: PROGRAMOZÁSEz 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

679

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

Item Item

Item Item

Page 680: PROGRAMOZÁSEz 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

680

ezeket, csak egy értéke van, de eldönthető óla, hogy levélcsúcs-e vagy belső

csúcs. Annak eldöntése, hogy egy csúcs levélcsúcs-e éppen az ellenkező

eredményt adja, mint amikor azt vizsgáljuk, hogy belső csúcs-e. Ezt az inverz

kapcsolatot a metódusok implementálásánál érdemes kihasználni. Az

absztrakt csúcs típusa is osztály-sablon (Node<Item>), hiszen egy csúcs

értékének típusát csak később szeretnénk megadni. Természetesen absztrakt

csúcsot nem lehet majd létrehozni, csak a láncolt csúcs osztályának

(LinkedNode<Item>) őséül szolgál, de lehetővé teszi például a tevékenység

objektumok Exec() műveleténél egy absztrakt csúcsra történő hivatkozást,

mert ott úgyis csak a csúcs értéke érdekel bennünket.

A modellünk akkor lesz konzisztens, ha az Action is osztály-sablon,

ennek is sablon-paramétere a bináris fa csúcsaiban tárolt értékek típusa.

A konkrét tevékenységek definiálásához több féle Action<Item>

osztály-sablonból származtatott tevékenység osztályt kell bevezetnünk.

Szükség lesz a teszteléshez egy kiíró (Printer) tevékenységre. Ez is

osztály-sablon, hiszen bármilyen típusú értékek kiírására képes, ha típusra

definiálták a kiíró operátort. Konkrét alkalmazásához ezért majd

példányosítani kell. Konstruktorának paraméterként adjuk meg azt a

kimeneti adatfolyamot, ahová írni szeretnénk. Az adatfolyamra történő

hivatkozást privát adattagként felvesszük az osztály-sablonba. Egy csúcs

értékének kiírását az Exec() metódus végzi.

Csak egész számokat tartalmazó bináris fára fogalmazzuk meg az

összegzés és a feltételes maximumkeresés tevékenységeket. Ezek tehát az

Action egész számokra példányosított változatából származtatott osztályok

lesznek. Az összegzést (Summation) a bináris fa csúcsaiban tárolt egész

számokra, a maximumkeresést (MaxSearch) a belső csúcsokban található

egész számokra (ez most a feltétel) fogalmazzuk meg.

Az összegzés esetében arra kell emlékeznünk, hogy az összegzés

programozási tételében szereplő ciklus előtt szerepel egy s:=0 inicializáló

lépés, a ciklusmagban pedig egy s:=s+aktuális érték értékadás, ahol az

aktuális értéket valamilyen felsoroló szolgáltatja. Most az s változót privát

adattagként vesszük fel a Summation osztályba, annak konstruktora végzi el

az s:=0 értékadást, és az Exec() metódusába kerül az s:=s+aktuális csúcs

Page 681: PROGRAMOZÁSEz 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

681

értéke értékadás. Az s változó értékét a bejárás végeztével a Result()

metódussal kérdezhetjük le.

A feltételes maximumkeresés esetében három privát adattagot

veszünk fel a MaxSearch osztályba: l logikai érték jelzi, hogy találtunk-e belső

csúcsot, a max változó annak a belső csúcsnak az értéke, amelyik a belső

csúcsok között a legnagyobb értékkel bír. A konstruktor az l:=hamis

értékadásból áll (ez a feltételes maximumkeresés tételében a ciklus előtti

értékadás). Az Exec() metódus a programozási tétel ciklusmagja: ha az

aktuális csúcs belső csúcs és korábban még nem találtunk belső csúcsot,

akkor l legyen igaz és a max vegye fel az aktuális csúcs értékét; ha l már igaz

volt, akkor hasonlítsuk össze a max-ot az aktuális csúcs értékével, és a kettő

közül a nagyobb legyen a max új értéke. Az l és a max értékét a bejárás után

a Found() és a MaxValue() metódusokkal kérdezhetjük le.

14-4. ábra. Tevékenységek osztálydiagrammja

Implementálás

A program komponens szerkezete

Item

Item Item

Page 682: PROGRAMOZÁSEz 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

682

A program két részből áll. A bintree.hpp állományban helyezzük el

az Action, Node, LinkedNode, BinTree osztály-sablonokat, a main.cpp

állomány tartalmazza a konkrét tevékenység osztályokat (Pinter,

Summation, Maxsearch) és main függvénybe ágyazott tesztprogramot.

Page 683: PROGRAMOZÁSEz 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

683

Action osztály

template < typename Item>

class Action{

public:

virtual void Exec(Node<Item> *node)=0;

};

Node osztály

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;

};

Page 684: PROGRAMOZÁSEz 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

684

LinkedNoded osztály

A Node osztály-sablonból származtatjuk a LinkedNode osztály-sablont.

Kiegészítjük a bal illetve jobboldali gyerekére mutató pointertagokkal,

felüldefiniáljuk az IsLeaf() metódust, hiszen most már a gyerekekre

mutató pointerek értéke alapján ez a tulajdonság kiszámolható, és ezzel

implicit módon az IsInternal()-t is definiáljuk.

Mivel meg akarjuk engedni, hogy a BinTree osztály-sablon lássa a

LinkedNode osztály-sablon privát tagjait, ezért a BinTree osztály-sablont

barátként kell megjelölni. Tekintettel azonban arra, hogy a fordító itt még

nem tudhatja mi az a BinTree, ezért a LinkedNode osztály-sablon előtt

deklarálni kell azt.

template < typename Item> class BinTree;

template < typename Item>

class LinkedNode: public Node<Item>{

friend class BinTree;

public:

LinkedNode(const Item& v,

LinkedNode *l, LinkedNode *r)

:Node<Item>(v), left(l), right®{}

bool IsLeaf() const

{return NULL == left && NULL == right;}

private:

LinkedNode *left;

LinkedNode *right;

};

Page 685: PROGRAMOZÁSEz 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

685

Bintree osztály

A bináris fa osztály-sablonjának publikus része elsőként egy üres fát

létrehozó konstruktort definiál. Egy fát megszüntető destruktort majd

később implementáljuk. A RandomInsert metódus segítségével

véletlenszerűen építünk fel egy bináris fát úgy, hogy megadva neki egy

értéket, ahhoz egy olyan új csúcsot generálunk a fában, amely ezt az értéket

tartalmazni fogja. Ehhez kapcsolódik a konstruktorban a véletlenszám

generátor srand(time(NULL))-lal (#include <cstdlib>, #include

<time.h>) történő életrehívása. A bináris fa osztály-sablonjában definiáljuk

a három nevezetes fa-bejárási stratégiát: a PreOrder(), InOrder() és

PostOrder() metódusokat. Ezek paramétere egy tevékenység objektum

címe, amelyet a gyökérelemtől indulva vezetnek végig a fa csúcsain.

A bináris fa osztály-sablonjának rejtett része tartalmazza a fa

gyökerére mutató root pointert, valamint a különböző stratégiájú

bejárásoknál meghívható Pre(), In() és Post() metódusokat. Ezeknek

egyik bemenő paramétere annak a csúcsnak a pointere, amely annak a

részfának a gyökerét jelzi, amelyre el akarjuk indítani a bejárást; a másik

annak a tevékenységnek a pointere, amit az egyes csúcsoknál végre kell

hajtani. Védettként deklaráljuk a másoló konstruktort és az értékadás

operátort, hogy letiltsuk a használatukat.

template < typename Item>

class BinTree{

public:

BinTree():root(NULL){srand(time(NULL));}

virtual ~BinTree();

void RandomInsert(const Item& e);

void PreOrder (Action<Item> *todo)

Page 686: PROGRAMOZÁSEz 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

686

{Pre (root, todo);}

void InOrder (Action<Item> *todo)

{In (root, todo);}

void PostOrder(Action<Item> *todo)

{Post(root, todo);}

protected:

LinkedNode<Item> *root;

void Pre(LinkedNode<Item> *r,Action<Item>*todo);

void In(LinkedNode<Item> *r, Action<Item>*todo);

void Post(LinkedNode<Item> *r,Action<Item>*todo);

BinTree(const BinTree&);

BinTree& operator=(const BinTree&);

};

A bináris fa bejáró műveletei a bejárásokat rekurzív módon írják le.

Ezen belül a todo által mutatott tevékenység Exec() metódusát kell az

aktuális csúcsra meghívni. Itt is tanúi lehetünk a dinamikus kötés

jelenségének. A todo->Exec() hívást ugyanis nem lehet fordítási időben

meghatározni (erre figyelmeztet az Exec() metódus virtuális volta), hiszen

nem tudhatjuk, hogy konkrétan milyen típusú tevékenység objektumra

mutat a todo.

Az alábbi metódusokban jól látható a három féle fabejárást végző

rekurzív algoritmus. Üres részfa esetén egyik sem kezdeményez rekurzív

hívást, nem üres részfa esetén a megfelelő sorrendben történik a részfa

gyökerének todo->Exec() általi feldolgozása és a bal illetve jobboldali

Page 687: PROGRAMOZÁSEz 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

687

részfára történő rekurzív hívás. Mivel egyre kisebb részfákra hívjuk meg az

alprogramot, véges lépésen belül üres részfákhoz fogunk jutni, azaz nem

fordulhat elő a rekurzív hívásoknak végtelen hosszú láncolata.

template < typename Item>

void BinTree<Item>::

Pre(LinkedNode<Item> *r,Action<Item> *todo)

{

if(NULL == r) return;

todo->Exec(r);

Pre(r->left, todo);

Pre(r->right, todo);

}

template < typename Item>

void BinTree<Item>::

In(LinkedNode<Item> *r, Action<Item> *todo)

{

if(NULL == r) return;

In(r->left, todo);

todo->Exec(r);

In(r->right, todo);

}

template < typename Item>

Page 688: PROGRAMOZÁSEz 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

688

void BinTree<Item>::

Post(LinkedNode<Item> *r,Action<Item> *todo)

{

if(NULL == r) return;

Post(r->left, todo);

Post(r->right, todo);

todo->Exec(r);

}

Érdekes és egyben hasznos alkalmazása a tevékenység objektumoknak

a bináris fa egy csúcsát megszüntető tevékenység létrehozása. Ennek típusát

az alábbi osztály írja le.

template < typename Item>

class DelAction: public Action<Item>{

public:

void Exec(Node<Item> *node){delete node;}

};

Ha példányosítunk egy ilyen tevékenység objektumot és végig vezetjük

őt a bináris fán a postorder bejárással, akkor ezzel felszabadítjuk a fa összes

csúcsát, azaz megszüntetjük a fát. A bináris fa destruktorának éppen erre van

szüksége. (Vigyázat! A másik két bejárás erre nem alkalmas.)

Ha a DelAction osztály definícióját a bináris fa osztály-sablonjának

rejtett részébe ágyazzuk, akkor egyrészt nem kell előtte feltüntetni a

template <class Item> sort, másrészt elég a destruktorban

DelAction-t írni a DelAction<Item> del helyett.

Page 689: PROGRAMOZÁSEz 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

689

template < typename Item>

BinTree<Item>::~BinTree()

{

DelAction del;

ost(root, &del);

}

Végül megadjuk a bináris fába új csúcsot véletlenszerűen beillesztő

metódus implementációját. Ez a metódus egy ciklusban generál

véletlenszerű „jobb illetve bal értékeket” amíg nem talál olyan csúcsot,

amelyiknek a legutoljára generált oldalán nincs csúcs. Ide függeszt fel a

beszúrandó értéket tartalmazó új csúcsot az alábbi kód.

template < typename Item>

void BinTree<Item>::RandomInsert(const Item& e)

{

if(NULL == root) root =

new LinkedNode<Item>(e,NULL,NULL);

else {

LinkedNode<Item> *r = root;

int d = rand();

while(d&1 ? r->left!=NULL : r->right!=NULL){

if(d&1) r = r->left;

else r = r->right;

d = rand();

Page 690: PROGRAMOZÁSEz 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

690

}

if(d&1) r->left =

new LinkedNode<Item>(e,NULL,NULL);

else r->right =

new LinkedNode<Item>(e,NULL,NULL);

}

}

A kódban néhány olyan érdekes C++ nyelvi elem került, mint a

feltételes kifejezés (feltétel ? kifejezés1 : kifejezés2), vagy a

bitenkénti „és” művelet, amellyel leválasztjuk a véletlen szám legutolsó

bitjét, hogy azt jobb illetve bal értéknek tekintsük.

Tevékenység osztályok

Egy tevékenység osztályt már definiáltunk, ez volt a DelAction. Adjuk meg

most a többit is.

A Maxsearch típusú tevékenység megkeresi a belső csúcsokban a

legnagyobb értéket. A konstruktorban a keresés sikerességét jelző logikai

értéket inicializáljuk, ennek, valamint a max tagnak az értékét módosítja az

Exec(), az eredményt pedig a Found() és a MaxValue() segítségével

kérdezhetjük le.

class Maxsearch: public Action<int>{

public:

Maxsearch(){l = false;}

void Exec(Node<int> *node){

if(node->IsLeaf()){

if(!l){

Page 691: PROGRAMOZÁSEz 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

691

l = true;

max = node->Value();

}else if(node->Value()>max)

max = node->Value();

}

}

bool Found(){return l;}

int MaxValue(){return max;}

private:

int max;

bool l;

};

A Summation típusú tevékenység hozzáadja az aktuális csúcs értékét

konstruktorban nullának inicializált s adattaghoz. A Result() ennek

aktuális értéket adja vissza.

Page 692: PROGRAMOZÁSEz 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

692

class Summation: public Action<int>{

public:

Summation(): s(0){}

void Exec(Node<int> *node){s+=node->Value();}

int Result(){return s;}

private:

int s;

};

A Printer osztály-sablon tevékenység objektumai egy csúcs értékét

írják ki. A tevékenység paramétere a konstruktorában beállítható kimeneti

folyam. Az osztály-sablonból egy konkrét kiíró tevékenység a Printer<int>

print(cout) utasítással hozható belőle létre.

template < typename Item>

class Printer: public Action<Item>{

public:

Printer(ostream &o): s(o){};

void Exec(Node<Item> *node)

{s << ’[’<< node->Value() << ’]’;}

private:

ostream& s;

};

Főprogram

Page 693: PROGRAMOZÁSEz 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

693

Az alábbi kód a szabványos bemenetről beolvasott értékekkel

véletlenszerűen épít fel egy bináris fát.

BinTree<int> t;

int i;

while(cin >> i) {

t.RandomInsert(i);

}

Ennek tartalmát különféle bejárási stratégiák mellett írjuk ki a standard

kimenetre, majd meghatározzuk a csúcsokban tárolt értékek összegét és a

belső csúcsok értékeinek maximumát.

Printer<int> print(cout);

cout << „Preorder bejárás:”;

t.PreOrder(&print);

cout << endl;

cout << „Inorder bejárás:”;

t.InOrder(&print);

cout << endl;

cout << „Postorder bejárás:”;

t.PostOrder(&print);

cout << endl;

Page 694: PROGRAMOZÁSEz 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

694

Summation sum;

t.PreOrder(&sum);

cout << „Fa elemeinek összege:”

<< sum.Result() << endl;

Maxsearch ms;

t.PreOrder(&ms);

cout << „Maximum of internal elements:\n”;

if(ms.Found()) cout << ms.MaxValue() << endl;

else cout << „none” << endl;

Page 695: PROGRAMOZÁSEz 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

695

Tesztelés

Egy olyan programot nehéz tesztelni, amelyik véletlenszerűen állítja elő a

bemenő adatokat. Szerencsére most nem egészen erről van szó, hiszen a

bináris fába betett értékeket mi adhatjuk meg, csak azok fába beillesztése

véletlenszerű. Ez nem akadályozza meg a fa kiírásának tesztelését, az

összegzés tesztelését, egyedül a maximumkeresés esetében nehéz az olyan

tesztadatok előállítása, amikor a legelső, vagy a legutolsó adatot szeretnék

maximálisnak választani. Ha másképpen nem megy, külön tesztprogramot

kell készíteni. Külön komponens tesztet nem készítünk, az alábbi tesztesetek

kielégítőek.

Fekete doboz tesztesetek:

Érvényes adatok:

1. Üres fa esete.

2. Egyetlen csúcs (gyökércsúcs, amely ilyenkor levél csúcs is) esete.

3. Két csúcs beillesztése. (Egy belső csúcs és egy levélcsúcs lesz)

4. Több csúcs, legalább két belső csúcs beillesztése. Ehhez

próbálgatással juthatunk el. (Itt érdemes az összegzést illetve a

feltételes maximumkeresést nemcsak a preorder, hanem a másik

kettő stratégiával is kipróbálni.)

5. Általános eset.

Érvénytelen adatok nem lehetnek, viszont a memória elfogyást ki lehet

mérni, de erre nem alkalmas a jelenlegi főprogram, hiszen az interaktív.

Fehér doboz tesztesetek:

1. Erre még inkább igaz az, amit a véletlen feltöltésről az előbb

mondtunk. Nehéz például letesztelni a RandomInsert() metódus

minden utasítását. Nagyszámú adat megadása esetén azonban

legalább 50%-os valószínűséggel minden utasítására rákerül a

vezérlés. Ugyanez mondható el a bejárások utasításairól is.

2. Dinamikus helyfoglalások miatt vizsgálni kellene még a

memóriaszivárgást.

Page 696: PROGRAMOZÁSEz 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

696

Teljes program

main.cpp:

#include "bintree.hpp"

#include <iostream>

using namespace std;

template < typename Item>

class Printer: public Action<Item>{

ostream& s;

public:

Printer(ostream &o): s(o){}

void Exec(Node<Item> *node)

{s << '['<< node->Value() << ']';}

};

class Summation: public Action<int>{

public:

Summation(): s(0){}

void Exec(Node<int> *node){s+=node->Value();}

int Result(){return s;}

private:

int s;

Page 697: PROGRAMOZÁSEz 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

697

};

class Maxsearch: public Action<int>{

public:

Maxsearch(){l = false;}

void Exec(Node<int> *node){

if(node->IsLeaf()){

if(!l){

l = true;

max = node->Value();

}else if(node->Value()>max)

max = node->Value();

}

}

bool Found(){return l;}

int MaxValue(){return max;}

private:

int max;

bool l;

};

Page 698: PROGRAMOZÁSEz 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

698

int main()

{

BinTree<int> t;

int i;

while(cin >> i) {

t.RandomInsert(i);

}

Printer<int> print(cout);

cout << "Preorder bejárás:";

t.PreOrder(&print);

cout << endl;

cout << "Inorder bejárás:";

t.InOrder(&print);

cout << endl;

cout << "Postorder bejárás:";

t.PostOrder(&print);

cout << endl;

Summation sum;

t.PreOrder(&sum);

Page 699: PROGRAMOZÁSEz 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

699

cout << "Fa elemeinek összege:"

<< sum.Result() << endl;

Maxsearch ms;

t.PreOrder(&ms);

cout << "Maximum of internal elements:\n";

if(ms.Found()) cout << ms.MaxValue() << endl;

else cout << "none" << endl;

return 0;

}

Page 700: PROGRAMOZÁSEz 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

700

bintree.hpp:

#ifndef BINTREE_H

#define BINTREE_H

#include <cstdlib>

#include <time.h>

template < typename Item>

class Node {

public:

Item Value() const {return val;}

virtual bool IsLeaf() const = 0;

bool IsInternal() const {return !IsLeaf();}

protected:

Node(const Item &v): val(v){}

Item val;

};

template < typename Item>

class Action{

public:

virtual void Exec(Node<Item> *node)=0;

Page 701: PROGRAMOZÁSEz 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

701

};

template < typename Item>

class BinTree{

public:

BinTree():root(NULL){srand(time(NULL));}

virtual ~BinTree();

void RandomInsert(const Item& e);

void PreOrder (Action<Item> *todo)

{Pre (root, todo);}

void InOrder (Action<Item> *todo)

{In (root, todo);}

void PostOrder(Action<Item> *todo)

{Post(root, todo);}

protected:

LinkedNode<Item> *root;

void Pre(LinkedNode<Item> *r,Action<Item>*todo);

void In(LinkedNode<Item> *r, Action<Item>*todo);

void Post(LinkedNode<Item> *r,Action<Item>*todo);

BinTree(const BinTree&);

BinTree& operator=(const BinTree&);

};

Page 702: PROGRAMOZÁSEz 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

702

template < typename Item>

BinTree<Item>::~BinTree()

{

DelAction del;

Post(root, &del);

}

template < typename Item>

void BinTree<Item>::RandomInsert(const Item& e)

{

if(NULL == root) root =

new LinkedNode<Item>(e,NULL,NULL);

else {

LinkedNode<Item> *r = root;

int d = rand();

while(d&1 ? r->left!=NULL : r->right!=NULL){

if(d&1) r = r->left;

else r = r->right;

d = rand();

}

if(d&1) r->left =

new LinkedNode<Item>(e,NULL,NULL);

else r->right =

Page 703: PROGRAMOZÁSEz 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

703

new LinkedNode<Item>(e,NULL,NULL);

}

}

template < typename Item>

void BinTree<Item>::

Pre(LinkedNode *r,Action<Item> *todo)

{

if(NULL == r) return;

todo->Exec(r);

Pre(r->left, todo);

Pre(r->right, todo);

}

template < typename Item>

void BinTree<Item>::

In(LinkedNode *r,Action<Item> *todo)

{

if(NULL == r) return;

In(r->left, todo);

todo->Exec(r);

In(r->right, todo);

}

Page 704: PROGRAMOZÁSEz 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

704

template < typename Item>

void BinTree<Item>::

Post(LinkedNode *r,Action<Item> *todo)

{

if(NULL == r) return;

Post(r->left, todo);

Post(r->right, todo);

todo->Exec(r);

}

#endif

Page 705: PROGRAMOZÁSEz 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

705

C++ kislexikon

absztrakt osztály class O{

// vagy privát konstruktor

private: Osztaly();

// vagy absztrakt metódus

void Method() = 0;

};

védett tag protected

publikus

származtatás

class O : public Os { };

privát

származtatás

class O : private Os { };

virtuális

metódus

virtual void Method();

dinamikus kötés class O : public OS { void M(); };

OS *p = new O();

p->M();

osztály-sablon template<typename T>

class O { … T … };

függvény-sablon template<typename T>

void Fv(… T …) { … T … }

operátor-sablon template<typename T>

void operator() { … T … }

példányosítás O<int> o;

Page 706: PROGRAMOZÁSEz 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

706

Fv<int>(…)

Page 707: PROGRAMOZÁSEz 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

707

15. Egy osztály-sablon könyvtár felhasználása

Ebben a fejezetben egy esettanulmányt találunk, amely egy osztály-sablon

könyvtárat és annak felhasználását mutatja be. A könyvtár a visszavezetéssel

tervezett programok C++-beli megvalósítását támogatja, és arra a

programozói stílusra támaszkodik, amely származtatással, a virtuális

metódusok felüldefiniálásával, valamint osztály-sablonok példányosításával

éri el egy már megírt kód újrahasznosítását.

A célunk az, hogy a felsorolóra megfogalmazott programozási

tételeket, helyesebben az arra visszavezetett programrészeket egy-egy

tevékenység-objektumként hajtsuk végre, egészen pontosan a tevékenység-

objektum 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ég-

objektumnak 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());

}

Page 708: PROGRAMOZÁSEz 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

708

A bemutatott osztály-sablon könyvtár nem az ipari alkalmazások

számára készült. Nem hisszük, hogy a gyakorlatban való felhasználása

egyszerű illetve célszerű lenne. Egy programozási tétel (lényegében egy

ciklus) implementálása ugyanis önmagában nem túl nehéz feladat, ezért

sokkal könnyebb közvetlenül kódolni, mint egy összetett osztály-sablon

könyvtárból származtatni, hiszen ehhez a könyvtár elemeit kell pontosan

megismerni és helyesen alkalmazni. Ennél fogva ez a tanulmány rávilágít

arra a határra is, hogy mikor érdemes az objektum-orientált illetve generikus

nyelvi eszközöket bevetni egy feladat csoport megoldásánál és ez mikor nem

jelent már előnyt. Ugyanakkor ez a könyvtár nagyon is alkalmas a különféle

objektum-orientált implementációs technikák megmutatására. A könyvtár

használatával igen szép megoldásokat tudunk előállítani. Programozói

szemmel például nagyon érdekes, hogy az előállított megoldásokban

mindössze egyetlen ciklus lesz, mégpedig a programozási tételek ősosztály-

sablonjá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ály-

sablonban rögzítjük.

15-1. ábra. Az felsorolók absztrakt osztály-sablon

Page 709: PROGRAMOZÁSEz 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

709

Minden olyan objektum, amelynek osztálya ebből származik,

rendelkezni fog a bejárás itt bevezetett négy alapműveletével. Ezek a

műveletek ezen a szinten nincsenek definiálva (absztraktak), a bejárt elemek

típusát pedig az Item sablonparaméter jelzi.

E fejezetben megoldott feladatok bemenő adatait egy szöveges

állomány tartalmazza, amelyet szekvenciális inputfájlként kívánunk

feldolgozni. Ezért célszerű kiegészíteni az osztály-sablon könyvtárat egy olyan

felsoroló osztállyal, amelynek objektumai szöveges állományra épített

szekvenciális inputfájl elemeit képesek sorban egymás után végigolvasni

(bejárni).

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.

Page 710: PROGRAMOZÁSEz 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

710

15-3. ábra. Tömb felsorolójának osztály-sablon

Ennek reprezentációja a tömb mellett egy indexváltozót is tartalmaz,

amelyet a First() művelet állít rá a tömb első elemére, a Next() növeli

meg eggyel az értékét, a Current() a tömb ennyiedik elemét adja vissza és

az End() akkor jelez majd igazat, ha az index túlhaladt a tömb végén.

A Procedure osztály-sablon a központi eleme a könyvtárnak. Minden

programozási tételnek ez az őse. Egyfelől definiál egy olyan metódust

(AddEnumerator), amellyel egy konkrét felsorolót (enor) lehet a

feldolgozáshoz hozzákapcsolni, másfelől tartalmazza ezen felsoroló által

bejárt elemeknek a bevezetőben már bemutatott feldolgozását végző Run()

metódust. A Run() közvetve vagy közvetlenül több olyan metódust is

meghív, amelyeket majd a származtatás során lehet vagy kell felüldefiniálni.

Ezek között az Init() és a Do() absztrakt metódusok, a többi rendelkezik

alapértelmezett működéssel.

Page 711: PROGRAMOZÁSEz 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

711

15-4. ábra. A programozási tételek ősosztály-sablonja

A Run() metódus végleges változata néhány részletében eltér a

bevezetőben vázolt verzióhoz képest. Egyrészt a felsoroló enor.First()

metódusa helyett egy olyan First() metódus hívását találjuk benne,

amelynek alapértelmezett definíciója éppen az enor.First() lesz, de ez

szükség esetén felülbírálható. Ez akkor hasznos, ha egy tevékenységet olyan

felsorolóval kell elvégezni, amelyet már korábban használtunk, de félbe

hagytuk, és most folytatni akarjuk a felsorolást. Ilyenkor nincs szükség újra az

enor.First() végrehajtására, ezért a First() metódust ilyenkor az üres

utasítással definiáljuk felül. Másrészt a !enor.End()ciklusfeltételt

kibővítjük (szigorítjuk) egy WhileCond()metódus hívásával, amely

alapértelmezés szerint mindig igaz értéket ad vissza (azaz nem vezet be

megszorítást), de ha kell, felüldefiniálható. Ezzel azt érhetjük el, hogy a

feldolgozás még az előtt álljon le, mielőtt a felsorolás véget érne. Sokszor kell

ugyanis egy programozási tételt úgy használni, hogy az a felsorolás vége előtt

egy speciális feltétel bekövetkezésekor véget érjen. (Például adjuk össze egy

sorozat számait, de csak az első negatív szám előttieket.) A WhileCond()

metódus értéke a felsorolás aktuális elemétől függ: amíg ez az elem kielégíti

az itt megadott feltételt, addig folytatódhat a feldolgozás. Harmadrészt az

így kibővített ciklusfeltételt a LoopCond() metódusban fogjuk össze, hogy

ez által lehetővé tegyük a ciklusfeltétel későbbi módosítását (mint ahogy ezt

a lineáris keresés és a kiválasztás megfogalmazásánál meg is tesszük majd).

Init();

for (First(); LoopCond(); enor.Next())

{

Do(enor.Current());

}

A Procedure osztályból származtatjuk a programozási tételek

osztályait.

Page 712: PROGRAMOZÁSEz 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

712

Az összegzés tételével többféle feladat-típust meg lehet oldani. A

szigorúan vett összegzés mellett ilyen például az összeszorzás, összefűzés,

feltételes összegzés, számlálás, másolás, kiválogatás, szétválogatás és

összefuttatás. Ezt az általánosságot tükrözi a Summation osztály-sablon.

Az Item sablonparaméter a feldolgozandó elemek típusára, a

ResultType paraméter az összegzés eredményének típusára utal. A

ResultType típusú result adattag az eredmény tárolására szolgál,

amelyet majd a Result() metódussal kérdezhetünk le.

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

Page 713: PROGRAMOZÁSEz 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

713

eggyel növelni. Az általános számlálásnál a Cond() nem kap új jelentést,

viszont egy konkrét számlálásnál ezt kell majd felüldefiniálni.

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.

Page 714: PROGRAMOZÁSEz 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

714

15-7. ábra. Az általános maximumkeresés osztály-sablonja

A Do() a feltételes maximumkeresés programozási tételéből ismert

hármas elágazást mutatja. Az Init() felüldefiniálásában a feltételes

maximumkeresésben is szereplő l logikai változót állítjuk be hamisra. A

Func() a maximumkeresés programozási tételében bevezetett f függvény,

amely egy elemhez azt az értéket rendeli, amely szerint az elemeket össze

kell hasonlítani, de ezen az általános szinten még nem lehet definiálni, ezért

absztrakt. A Cond()a keresési feltételt írja le, alapértelmezett jelentése igaz

(ha ezt megtartjuk, akkor az általános maximumkeresés egy közönséges

maximum kiválasztás lesz).

A kiválasztás osztály-sablonja implementálja az Init()és a Do()

metódusokat. valamint felülírja a LoopCond()metódust.

Page 715: PROGRAMOZÁSEz 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

715

15-8. ábra. A kiválasztás osztály-sablonja

Sajátos módon az Init()és a Do() metódus az üres utasítás lesz. A

LoopCond() metódust úgy kell felüldefiniálni, hogy éppen annak az

absztrakt Cond() metódusnak a tagadottja legyen, amelynek egy konkrét

kiválasztási feladatnál történő felüldefiniálásával a keresett elem

tulajdonságát írhatjuk le.

A lineáris keresés osztály-sablonja biztosítja mind a normális

(pesszimista), mind a tagadott (optimista) lineáris keresés előállítását.

A pesszimista („úgy sem fogunk találni megfelelő elemet, de

próbálkozzunk”) lineáris keresés egy felsorolásnak az első adott tulajdonságú

elemét (elem) keresi meg. Egyfelől megmondja, hogy talált-e egyáltalán ilyet

(l), és ha igen tárolja az elsőt (elem). Az optimista („nyilván minden elem

megadott tulajdonságú, de a biztonság kedvéért nézzük át őket”) lineáris

keresés azt dönti el, hogy a felsorolás minden eleme rendelkezik-e a

megadott tulajdonsággal (l), és ha nem, az első nem ilyen tulajdonságút adja

meg (elem). A keresés eredménye a Found() és Elem() publikus

metódusokkal kérdezhető le. A Do() metódust a lineáris keresések

ciklusmagjának megfelelően implementáljuk, amely a β feltételt (lásd előző

kötet) helyettesítő Cond() metódust hívja. A metódus az l logikai változót

igaz-ra állítja, ha az aktuális elem kielégíti a keresett tulajdonságot és ekkor

az elem az aktuális elemet kapja értékül, különben az l hamis lesz.

Page 716: PROGRAMOZÁSEz 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

716

15-9. ábra. A lineáris keresés osztály-sablonja

Azt, hogy a keresés pesszimista vagy optimista legyen, egy külön

sablon-paraméterrel állíthatjuk be. Alapértelmezés szerint ez a paraméter

hamis, amely a pesszimista lineáris keresést definiálja, mert ennél kezdetben

hamis értéket adunk a keresés logikai változójának. Az optimista lineáris

keresést a sablon-paraméter igaz értéke jelzi, mert ekkor a logikai változót a

keresés elején igaz-ra kell állítani. A keresést tehát a sablon-paraméter

értékétől függően kell inicializálni (Init()), de a ciklusfeltétele

(LoopCond()) is ennek megfelelően változik. Ezért mindkettőt felül kell

definiálnunk. A keresett tulajdonságot a Cond() absztrakt metódus adja,

amelyet majd a konkrét kereséseknél kell felüldefiniálni.

Végezetül tekintsük át a teljes osztály könyvtárat az osztályok közötti

kapcsolatokkal együtt. A programozási tételeket leíró osztályok véglegesen

definiálják a Do() metódust, és az összegzés kivételével az Init()

metódust is. Véglegesek a publikus metódusok, a számlálásnál az Add(), a

kiválasztásnál és a lineáris keresésnél a LoopCond().

Page 717: PROGRAMOZÁSEz 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

717

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.

Item Item

Item Item

Item Item

Item

Item Item,optimist Item,Value,Compare Item,ResultType

Page 718: PROGRAMOZÁSEz 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

718

template <typename Item>

class Enumerator{

public:

virtual void First() = 0;

virtual void Next() = 0;

virtual bool End() const = 0;

virtual Item Current() const = 0;

virtual ~Enumerator(){}

};

Az End() és a Current() konstans metódusok, és felveszünk egy

virtuális üres destruktort is.

A szekvenciális inputfájlok felsorolójának osztályát az alábbi kód írja

le.

template <typename Item>

class SeqInFileEnumerator : public Enumerator<Item>{

protected:

std::ifstream f;

Item df;

public:

enum Exceptions { OPEN_ERROR };

SeqInFileEnumerator(const std::string& str){

f.open(str.c_str());

if(f.fail()) throw OPEN_ERROR;

if(typeid(Item) == typeid(char))

Page 719: PROGRAMOZÁSEz 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

719

f.unsetf(std::ios::skipws);

}

void First() {Next();}

void Next() {f >> df;}

bool End() const {return f.fail();}

Item Current() const {return df; }

};

Az osztálynak két adattagja van: egy szöveges állományra nyitott

adatfolyam (f) és a legutoljára beolvasott Item típusú elem (df).

A konstruktor kapja meg a szöveges állomány nevét, amelyre az input

adatfolyamot kell megnyitni. A konstruktor nem létező állománynév esetén

kivételt dob. Azt, hogy milyen típusú értékeket kell a szöveges állományból

beolvasni, az Item sablon-paraméter konkrét értéke dönti el. Mivel az

olvasás alapértelmezés szerint átlépi a szóközöket, tabulátorjeleket és

sorvége-jeleket, ezért amennyiben a szöveges állomány karaktereit kellene

egyesével beolvasni, azaz Item értéke char, akkor a konstruktor kikapcsolja

az elválasztó jeleket átlépő automatizmust. Ehhez szükség van az #include

<typeinfo> utasításra is.

A szöveges állományból való olvasás a >> operátorral történik, ezt

használja a First() és a Next() művelet is. A legutoljára beolvasott és a

df-be került elemet a Current() metódussal kérdezhetjük le, az End()

metódus pedig az adatfolyam olvasási hibájakor, ilyen hiba a fájlvége

bekövetkezése is, ad vissza igaz értéket.

A tömbök felsorolójának osztályát nem részletezzük.

A feldolgozások ősosztályának, azaz a Procedure osztály-sablonnak

enor adattagja egy felsorolóra mutató pointer, amelyet majd az

AddEnumerator metódusa inicializál. A Run() hívja meg az Init(),

First(), LoopCond() és Do() virtuális metódusokat, a LoopCond()

pedig a WhileCond() metódust. Az Init() és a Do()absztraktak, a többi

Page 720: PROGRAMOZÁSEz 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

720

rendelkezik alapértelmezett definícióval (ezek szerepéről korábban már

szóltunk). Mind a Do(), mind a WhileCond() metódusok paraméterként

kapják meg az éppen felsorolás alatt álló Item típusú elemet.

template <typename Item>

class Procedure{

protected:

Enumerator<Item> *enor;

Procedure():enor(NULL){}

virtual void Init()= 0;

virtual void Do(const Item& current) = 0;

virtual void First() {enor->First();}

virtual bool WhileCond(const Item& current)const

{return true;}

virtual bool LoopCond() const{

return !enor->End() &&

WhileCond(enor->Current());

}

public:

enum Exceptions {MissingEnumerator};

void Run();

void AddEnumerator(Enumerator<Item>* en)

{enor = en;}

virtual ~Procedure(){}

Page 721: PROGRAMOZÁSEz 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

721

};

Annak érdekében, hogy a feldolgozás ne kezdődhessen el anélkül,

hogy rendelkezünk felsorolóval, a Run() metódusban megvizsgáljuk a

felsoroló állapotát: ha nem létezik (NULL==enor), dobunk egy

MissingEnumerator kivételt.

template <typename Item>

void Procedure<Item>::Run(){

if(NULL == enor) throw ExpectedEnumerator;

Init();

for( First(); LoopCond(); enor->Next()){

Do(enor->Current());

}

}

Az összegzés osztály-sablonjában az eredményre a result adattaggal

hivatkozunk. A megvalósításban ez egy pointer lesz, amelynek muszáj

hivatkozni egy objektumra. Ezt az objektumot létrehozhatjuk helyben (ezt

teszi az első konstruktor), és ilyenkor gondoskodnunk kell a felszabadításáról

is (destruktorban), de átadhatjuk a (második) konstruktor paramétereként is.

Ezt tesszük majd akkor, ha például az összegzés eredményét közvetlenül egy

adatfolyamba kell írni, és ilyenkor a result-nek az adatfolyam

objektumának címét adjuk. A kétféle eset nyilvántartásához felveszünk a

tervben még nem szereplő új adattagot, a logikai inref-et, amely akkor lesz

igaz, ha belül történik a helyfoglalás az eredmény objektum számára.

Megjegyezzük, hogy a második konstruktor használata csak feltételezi, de

bizonyosságot nem szerez arról, hogy az eredmény számára tényleg

Page 722: PROGRAMOZÁSEz 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

722

megtörtént-e máshol a helyfoglalás. A *result értékét a Result()publikus

metódussal kérdezhetjük le.

template < typename Item, typename ResultType=Item >

class Summation : public Procedure<Item>{

protected:

ResultType *result;

bool inref;

Summation(){

{ inref = true; result = new ResultType; }

Summation(ResultType *r){

{ inref = false; result = r; }

void Do(const Item& e){ if(Cond(e)) Add(e);}

virtual void Add(const Item& e) = 0;

virtual bool Cond(const Item& e) const

{ return true; }

public:

ResultType Result() { return *result; }

~Summation(){ if(inref) delete result;}

};

A Summation-ből származtatott osztályokban már nem változtatjuk

meg a Do() metódust, de az Init()és az Add()metódust felül kell, a

Cond(), valamint a WhileCond() és a First() metódusokat felül lehet

definiálni. Egy konkrét összegzés megadásakor *result-nek lehet

kezdőértéket adni az Init() metódusban, és ennek lehet megváltoztatni az

Page 723: PROGRAMOZÁSEz 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

723

értékét azzal az Add() metódussal, amelyik megkapja a felsorolás aktuális

elemét bemenő paraméterként.

Egy jó példa konkrét összegzésre a számlálás.

template < typename Item >

class Counting : public Summation<Item, int>{

public:

Counting():Summation<Item,int>(){}

protected:

void Init(){*Summation<Item,int>::result = 0;}

void Add(const Item& e)

{++*Summation<Item,int>::result;}

};

Ez az osztály-sablon az összegzés osztály-sablon újrahasznosításával

készül, hiszen speciális összegzésként fogható fel. Ebben a ResultType az

int lesz, az Init()-et a *result=0 értékadással, az Add()-ot pedig a

++*result utasítással implementáljuk.

Az Init() és az Add()metódusok definíciója végleges, a Counting

leszármazottaiban csak a Cond()-ot kell majd felüldefiniálni, valamint a

WhileCond() és a First() metódusokat lehet módosítani, ha erre szükség

van.

Az általános maximumkeresés talán a legösszetettebb osztály-

sablonunk. Implementálja a Do() és Init() metódusokat, továbbá bevezet

két később felüldefiniálandó virtuális metódust: Func() és Cond().

A better típusát sablon-paraméterrel (Compare) lehet majd

megadni. A paraméter helyébe adott típusnak definiálnia kell a zárójel

operátort (operator()). Ekkor ugyanis a better(left,right) kifejezés

a better objektum operator()-át hívja a két paraméterrel. Ha az operátor

Page 724: PROGRAMOZÁSEz 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

724

akkor ad vissza igazat, ha mondjuk az első paraméter nagyobb a másodiknál,

akkor a better(left,right) éppen azt az összehasonlítást végzi

(left>right), amelyre a maximumkeresésnek szüksége van.

Természetesen ilyenkor a „>” operátornak értelmezettnek kell lenni a Value

sablon-paraméter helyébe írt típuson. Ha a Compare sablon-paraméterhez a

Greater osztály-sablont rendeljük (ez az alapértelmezés), akkor a

MaxSearch a maximumot fogja előállítani.

template <typename Value>

class Greater{

public:

bool operator()(const Value& left,

const Value& right)

{ return left > right; }

};

A minimumkereséshez a „kisebb” relációt használó összehasonlító

osztályra van szükség. A Less osztály-sablon a fenti Greater mintájára

készül, egyetlen különbség az, hogy a „>” operátor helyett a „<” operátort

tartalmazza. Ne feledjük, hogy a „<” operátornak értelmezettnek kell lenni a

Value sablon-paraméternek adott típuson.

template < typename Item, typename Value = Item,

typename Compare = Greater<Value> >

class MaxSearch : public Procedure<Item>{

protected:

bool l;

Item optelem;

Page 725: PROGRAMOZÁSEz 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

725

Value opt;

Compare better;

void Init(){ l = false;}

void Do(const Item& current);

virtual Value Func(const Item& e) const = 0;

virtual bool Cond(const Item& e) const

{return true;}

public:

bool Found() const {return l;}

Value Opt() const {return opt;}

Item OptElem() const {return optelem;}

};

template < typename Item, typename Value,

typename Compare >

void MaxSearch<Item,Value,Compare>::

Do(const Item& current){

Value val = Func(current);

if ( !Cond(current) ) return;

if (l){

if (better(val,opt)){

opt = val; optelem = current;

Page 726: PROGRAMOZÁSEz 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

726

}

}else{

l = true; opt = val; optelem = current;

}

}

A MaxSearch osztálynak a származtatása során felül kell definiálni a

Func() metódust, és felül lehet definiálni a Cond(), WhileCond() és

First() metódusokat.

A kiválasztás megvalósítása nem igényel különleges lépéseket. Az

Init(), a Do()és a LoopCond() metódusok definíciója végleges, a

leszármazottakban csak a Cond()-ot kell majd megadni. Módosítható

marad a First(), és elvileg módosítható a WhileCond()metódus is, de

ennek nem lesz a kiválasztásra semmi hatása.

template < typename Item >

class Selection : public Procedure<Item>{

protected:

void Init(){}

void Do(const Item& e) {}

bool LoopCond() const {

return !Cond(

Procedure<Item>::enor->Current());

}

virtual bool Cond(const Item& e) const = 0;

public:

Page 727: PROGRAMOZÁSEz 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

727

Item Elem() const

{return Procedure<Item>::enor->Current();}

};

A lineáris keresés osztály-sablonjának megvalósítása is a tervben

leírtakat követi. Azt Init(), a LoopCond() és a Do() metódust itt

véglegesen definiáljuk, a keresés feltételét leíró Cond() absztrakt metódust

viszont a leszármazott osztályokban kell majd megadni, és ott módosítható a

WhileCond() és a First() metódus is.

template < typename Item, bool optimist = false >

class LinSearch : public Procedure<Item> {

protected:

bool l;

Item elem;

void Init() {l = optimist; }

void Do(const Item& e) {l = Cond(elem = e);}

bool LoopCond() const{

return (optimist?l:!l)

&& Pocedure<Item>::LoopCond();

}

virtual bool Cond(const Item& e) const = 0;

public:

bool Found() const { return l;}

Page 728: PROGRAMOZÁSEz 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

728

Item Elem() const { return elem;}

};

Page 729: PROGRAMOZÁSEz 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

729

38. Feladat: Kiválogatás

Adott egy egész számokat tartalmazó szöveges állomány. Válogassuk ki

belőle a páratlan számokat!

Megoldási terv

A feladat egy kiválogatás, amely visszavezethető az összegzés programozási

tételére úgy, hogy azt egy szekvenciális inputfájl elemeit bejáró felsorolóra

kell azt alkalmazni, az összeadás művelete helyett pedig az eredmény-

sorozatba történő kiírásra lesz szükség, de csak akkor, ha az aktuális elem

páratlan.

A = ( f : enor(ℤ), cout : ℕ* )

Ef = ( f=f’ )

Uf = ( cout =

e

páratlane

fe ' )

Implementálás

Az osztály-könyvtár segítségével a feladat megoldása igen egyszerű.

Először példányosítással és származtatással elkészítjük az összegzésből azt a

kiválogatást, amely példánya (objektuma), mint tevékenység, képes a feladat

megoldására.

A származtatás előtt a sablon-paraméterek konkrét értékeit is meg kell

adni. A feldolgozandó elemek most egész számok (int), mely az Item

sablon-paramétert helyettesíti, az eredmény típusa (ResultType) a

kimeneti adatfolyam (ostream) lesz. Az eredmény (a szabványos kimeneti

adatfolyam, azaz a cout) címét a konstruktorral adjuk át a kiválogatásnak.

Erre az adatfolyamra a Selecting osztályon belül a result-tel tudunk

hivatkozni.

Az Init() implementálásához most elég ez üres utasítás. A

származtatott Do() eljárás a felsoroló által adogatott összes egész számra

meghívódik, és akkor hívja meg az Add() metódust, ha a Cond() igazat ad.

Ezért ha az Add() metódust a {*result << e;}-vel, a Cond() metódust a

Page 730: PROGRAMOZÁSEz 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

730

{ return e%2!=0;}-val írjuk felül, akkor akkor csak a páratlan számok

kerülnek kiírásra.

class Selecting : public Summation<int,ostream>{

public:

Selecting(ostream *o):

Summation<int,ostream>(o){}

protected:

void Init(){}

void Add(const int& e) {*result <<setw(5) <<e;}

bool Cond(const int& e) const { return e%2!=0;}

};

Ennek az osztálynak a segítségével hozzuk létre a főprogramban a

kiválogatást végző pr tevékenység-objektumot, amelynek átadjuk a cout

címét (ahová írni akarunk), majd hozzáadjuk az "inp.txt" szöveges

állomány egész számait kiolvasni képes SeqInFileEnumerator<int>

típusú felsoroló-objektum címét.

Selecting pr(&cout);

SeqInFileEnumerator<int> f("inp.txt");

pr.AddEnumerator(&f);

pr.Run();

A pr.Run() végrehajtja a kívánt kiválogatást.

Tesztelés

Page 731: PROGRAMOZÁSEz 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

731

Magát az osztály-sablon könyvtárat nem kell tesztelni, de a származtatás

során beállított paramétereket igen, ami a feladat szokásos, az alkalmazott

programozási tételt középpontba állító fekete-doboz tesztelését igényli.

Érvényes adatok:

1. Üres bemeneti állomány.

2. Egyetlen elemet: páros illetve páratlan számot tartalmazó bemeneti

állomány.

3. Csupa páros számot tartalmazó bemeneti állomány, a páros számok

között szerepelnek negatív számok és a nulla is.

4. Csupa páratlan számból álló bemeneti állomány, köztük az 1 és a -1.

5. Vegyes számsorozat.

Érvénytelen adatok:

1. Nem létező bemeneti állomány.

A fehér doboz tesztelés a kiírás formájának és a kivétel lekezelésének

ellenőrzését igényli csak.

Teljes program

A tejes saját kódot a main.cpp állományba tesszük.

#include <iostream>

#include <iomanip>

#include "seqinfileenumerator.hpp"

#include "summation.hpp"

using namespace std;

class Selecting : public Summation<int,ostream>{

Page 732: PROGRAMOZÁSEz 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

732

public:

Selecting(ostream *o):

Summation<int,ostream>(o){}

protected:

int db;

void Init(){ db = 0;}

void Add(const int& e) {*result <<setw(5) <<e;}

bool Cond(const int& e) const { return e%2!=0;}

};

int main()

{

try{

Selecting pr(&cout);

SeqInFileEnumerator<int> f("inp.txt");

pr.AddEnumerator(&f);

cout << "Az állomány páratlan számai:\n";

pr.Run();

}catch(SeqInFileEnumerator<int>::Exceptions ex){

cout << "Nem létező bemeneti fájl!\n";

}

return 0;

Page 733: PROGRAMOZÁSEz 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

733

}

Page 734: PROGRAMOZÁSEz 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

734

39. Feladat: Feltételes maximumkeresés

Egy forgalom számlálás során egy hónapon keresztül számolták, hogy egy

nap adott órájában hány utas lép be egy adott metróállomás területére.

(Lehet, hogy nem minden nap végeztek megfigyelést.) Az órákat egy olyan

négyjegyű számmal kódolták, amelynek első két jegye a nap hónapbeli

sorszámát, utolsó két jegye az órát mutatja. Egy szöveges állományban kód-

létszám párok formájában rögzítették az adatokat. (Tegyük fel, hogy a hónap

első napja hétfőre esett.) Melyik hétvégi nap melyik órájában léptek be

legkevesebben az állomás területére?

Megoldási terv

Ez a feladat egy feltételes maximumkereséssel oldható meg, amely a

szöveges állományból beolvasott azon szám-párok második tagja feletti

minimumkeresés, amelyek első tagjának százzal vett osztási eredménye 7-tel

osztva 6-ot vagy 0-t ad maradékul (azaz hétvégi napról van szó).

A = ( f : enor(Mérés), l : 𝕃, idő : ℕ )

Mérés = rec(idő : ℕ , létszám : ℕ)

Ef = ( f=f’ )

Uf = ( l, min, elem = létszáme

hétvégeidőe

.

.t'e

min

l idő = elem.idő )

Implementálás

A mérések Pair típusának két adata van: az időpont-kód és a létszám.

Fontos metódusai az idő kódból visszafejthető nap és óra. A Pair típusra be

kell vezetni egy mérésnek egy adatfolyamból történő beolvasását végző

operator>>-t, mert a szöveges állományt egy

SeqInFileEnumerator<Pair> típusú felsorolóval szeretnénk bejárni, és

ennek Next() művelete igényli ezt az operátort. A beolvasó operátor egy

mérést két lépésben, az időkódnak és a létszámnak a beolvasásával olvas be.

Page 735: PROGRAMOZÁSEz 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

735

Az operátor friend tulajdonsága itt nem is kell, lévén a Pair csak egy

struct és nem class.

struct Pair{

int time;

int number;

int Day() const { return time/100; }

int Hour() const { return time%100; }

};

ifstream& operator>>(ifstream& f, Pair& df){

f >> df.time >> df.number;

return f;

}

Ezek után a konkrét feladat feltételes minimumkeresés osztályát

készítjük el. Példányosítjuk a MaxSearch osztály-sablont: a Pair lesz az

elemek (Item) típusa, int az elemekhez rendelt értékek (Value) típusa

(ezek alapján keressük a legkisebbet), Less<int> a minimumkeresést

kijelölő összehasonlító (Compare) osztály. Ebből származtatjuk a

MyMinSearch osztályt, ahol felüldefiniáljuk a Func() és a Cond() virtuális

tagfüggvényeket. A Func()egy méréshez (azaz egy értékpárhoz) rendeli

annak a létszám komponensét, hiszen ez alapján szeretnénk összehasonlítani

a méréseket. A Cond() egy mérésre akkor ad igazat, ha annak időpontja

hétvégére esik.

class MyMinSearch:

public MaxSearch<Pair, int, Less<int> > {

protected:

Page 736: PROGRAMOZÁSEz 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

736

int Func(const Pair &e) const

{ return e.number; }

bool Cond(const Pair &e) const

{ return (e.Day()+1)%7==0 || e.Day()%7==0; }

};

A főprogram szerkezete nagyon hasonlít az előző feladat

főprogramjához.

MyMinSearch pr;

SeqInFileEnumerator<Pair> f("input.txt");

pr.AddEnumerator(&f);

pr.Run();

Csak az eredmény kiírásából látszik, hogy itt egy új feladatot oldottunk

meg.

if (pr.Found()){

Pair p = pr.OptElem();

cout << "Hétvégi napok közül " << p.Day()

<< ". napon " << p.Hour()

<< "-kor volt a legkevesebb, "

<< pr.Opt() << " fő az állomáson.\n";

}else cout << "Nincs hétvégi adat" << endl;

Page 737: PROGRAMOZÁSEz 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

737

Tesztelés

A saját kód tesztelése a feltételes maximumkereséseknél alkalmazott fekete

doboz (érvényes és érvénytelen) tesztadatok vizsgálatát jelenti, a fehér

doboz teszteseteit – lévén a saját kód igen egyszerű – a fekete doboz

tesztesetek lefedik. A fekete doboz tesztesetek összeállítását az Olvasóra

bízzuk.

Page 738: PROGRAMOZÁSEz 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

738

Teljes program

#include <iostream>

#include "seqinfileenumerator.hpp"

#include "maxsearch.hpp"

using namespace std;

struct Pair{

int time;

int number;

int Day() const { return time/100; }

int Hour() const { return time%100; }

};

class MyMinSearch:

public MaxSearch<Pair, int, Less<int> > {

protected:

int Func(const Pair &e) const

{ return e.number; }

bool Cond(const Pair &e) const

{ return (e.Day()+1)%7==0 || e.Day()%7==0; }

};

int main()

{

Page 739: PROGRAMOZÁSEz 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

739

try{

MyMinSearch pr;

SeqInFileEnumerator<Pair> f("inp.txt");

pr.AddEnumerator(&f);

pr.Run();

if (pr.Found()){

Pair p = pr.OptElem();

cout << "Hétvégi napok közül "

<< p.Day() << ". napon "

<< p.Hour()

<< "-kor volt a legkevesebb, "

<< pr.Opt() << " fő az állomáson.\n";

}else cout << "Nincs hétvégi adat" << endl;

}catch(SeqInFileEnumerator<Pair>::Exceptions ex){

{ cout << "Nem létező bemeneti fájl!\n"; }

return 0;

}

ifstream& operator>>(ifstream& f, Pair& df){

f >> df.time >> df.number;

return f;

}

40. Feladat: Keresés

Page 740: PROGRAMOZÁSEz 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

740

Egy szöveges állományban neveket soroltak fel ábécé szerint rendezve.

Keressük meg az első olyan nevet, amelyik legalább ötször fordul elő!

Megoldási terv

A megoldáshoz lineáris keresésre lesz szükségünk, amely olyan felsorolóra

támaszkodik, amelyik nem egyszerűen a szöveges állománybeli neveket

sorolja fel, hanem minden nevet csak egyszer, de úgy, hogy minden névhez

megadja, hogy az hányszor fordult elő. Ez a felsoroló tehát összegzi az azonos

nevek – ezek a rendezettség miatt közvetlenül egymás után állnak – számát.

A = ( t : enor (Pár), l : 𝕃, név : String )

Pár = rec(név : String , db : ℕ)

Ef = ( t=t’)

Uf = ( l,p, t = 5

dbete

.'

search l név=p.név )

A t felsoroló megvalósításához szükség lesz a szöveges állomány

neveinek közönséges felsorolására is. Az ezt elvégző f felsoroló része lesz az

absztrakt t felsoroló reprezentációjának, csakúgy, mint a felsorolás során

legutoljára megszámolt név és annak darabszáma (akt), valamint a vége

logikai érték, amelyik akkor mutat igaz értéket, ha már nincs több

felsorolandó név-darabszám pár. A t felsoroló műveleteinek

implementációja az alábbi.

enor(Pár) First() Next() End() Current()

f : enor(Szó)

akt : Pár

vége : 𝕃

f.First()

Next()

vége:=f.End()

ha vége akkor

akt.név:=f.Current()

összegez(akt, f)

vége akt

Page 741: PROGRAMOZÁSEz 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

741

Az összegez az akt.név-vel rendelkező neveket számolja meg és az

eredményt az akt.db-ben helyezi el. Ehhez egy olyan összegzésre van

szükség, amely folytatja a névfelsorolást az f-fel, és addig tart, amíg vagy

másik nevet nem olvasunk, vagy véget érnek a nevek. Ez tehát egy

előreolvasást nem igénylő feltétel fennállásáig tartó összegzés.

Implementálás

Mindenekelőtt definiáljuk a név-daraszám párok típusát.

struct Pair{

string name;

int count;

};

A feladatot megoldó lineáris kereséshez csak a keresési feltételt kell

megadnunk.

class MyLinSearch : public LinSearch<Pair>{

public:

bool Cond(const Pair &e) const

{return e.count>=5;}

};

A főprogramban main függvénye majdnem olyan egyszerű, mint az

előző feladatoknál, csak most egy egyedi típusú (PairEnumerator)

felsoroló objektumot kell használnunk, hiszen a tervezésnél egy absztrakt

felsorolót álmodtunk meg.

Page 742: PROGRAMOZÁSEz 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

742

MyLinSearch pr;

PairEnumerator t("inp.txt");

pr.AddEnumerator(&t);

pr.Run();

if (pr.Found())

cout << "A " << pr.Elem().name << " az első";

else cout << "Nincs";

cout << " név, amelyből legalább öt van. \n";

A párokat felsoroló absztrakt objektum (t) típusát leíró osztályt nekünk

kell definiálni. Ez az Enumerator Pair-re példányosított osztályából

származik és megvalósítja a felsoroló műveleteket. Védett adattagjai a

tervezésnél megadott reprezentáló elemek.

class PairEnumerator : public Enumerator<Pair>{

protected:

SeqInFileEnumerator<std::string> *f;

Pair current;

bool end;

public:

PairEnumerator(const std::string& str){

try{ f =

new SeqInFileEnumerator<std::string>(str);

}catch(SeqInFileEnumerator<std::string>::

Page 743: PROGRAMOZÁSEz 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

743

Exceptions ex){

std::cout << "Nem létező fájl!\n";

exit(1);

}

}

~PairEnumerator(){ delete f; }

void First() { f->First(); Next(); }

void Next();

bool End() const { return end;}

Pair Current() const { return current; }

};

Ez a felsoroló egy szöveges állomány neveinek felsorolására épül, ezért

a reprezentálásához egy olyan SeqInFileEnumerator típusú felsorolót kell

bevezetnünk (ez lesz a tervben szereplő f), amely erre képes. Ezt a párokat

felsoroló objektum konstruktora hozza létre és a destruktora szünteti meg.

Az absztrakt felsoroló műveletek közül az End() és a Current() nem túl

bonyolult, a felsorolás aktuális elemét illetve a felsorolást végét jelző logikai

értéket adják vissza. A First() művelet csak annyiban tér el a Next()-től,

hogy tartalmazza a legelső név kiolvasását is. A Next() metódus egy

elágazás, amely többek között az egymás után álló azonos nevek

összeszámlálását végzi.

Készítsük el először az összeszámlálást megoldó, feltételig tartó,

előreolvasás és feltétel nélküli számlálás4 osztályát (NameCounting). Az

előreolvasást a First()metódus üres utasítással való felülírásával tilthatjuk

le. A terminálási feltételt a WhileCond() felülírásával állíthatjuk be úgy,

4 Egy feltétel nélküli számlálás valójában egy összegzés.

Page 744: PROGRAMOZÁSEz 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

744

hogy akkor adjon igazat, ha az aktuális név megegyezik az összeszámolni

kívánt névvel. Ez utóbbit a számlálás konstruktorán keresztül lehet majd

megadni, és egy erre alkalmas tagváltozóban (name) fogjuk tárolni. Különös

módon éppen a számlálás feltételét megadó Cond() metódust nem szabad

most felüldefiniálni, mert annak alapértelmezett definíciója kell, amely

mindig igazat add vissza. Ettől lesz a számlálás feltétel nélküli.

class NameCounting : public Counting<string>{

public:

NameCounting(const string &str):

Counting<string >(), name(str){}

protected:

string name;

bool WhileCond(const string& e) const

{ return e == name; }

void First(){}

};

A Next() metódus ezek után már könnyen implementálható. A

feltétel nélküli számláláshoz ugyanazt az f felsorolót kell hozzáadni, amely az

absztrakt felsorolót reprezentálja.

void PairEnumerator::Next(){

if(end = f->End()) return;

current.name = f->Current();

NameCounting pr(current.name);

pr.AddEnumerator(f);

Page 745: PROGRAMOZÁSEz 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

745

pr.Run();

current.count = pr.Result();

}

Tesztelés

Elegendő az alkalmazott programozási tételeket középpontba állító fekete-

doboz tesztelést elvégezni.

Érvényes adatok:

1. Üres inputfájl.

2. Csupa különböző név.

3. Egyik névből sincs öt vagy annál több.

4. Csak az első, csak egy közbülső, csak az utolsó névből van öt darab.

5. Több névből is van legalább öt darab.

6. Minden névből legalább öt darab van.

Érvénytelen adatok:

1. Nem létező állománynév.

Teljes program

A saját kód több forrásállományban helyezkedik el. Külön fordítási egységet

alkot a párok felsorolását definiáló osztály, és az ahhoz szükséges egyéb

struktúrák és osztályok.

main.cpp:

#include <iostream>

#include "pairenumerator.h"

#include "linsearch.hpp"

using namespace std;

Page 746: PROGRAMOZÁSEz 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

746

class MyLinSearch : public LinSearch<Pair>{

public:

bool Cond(const Pair &e) const

{ return e.count>=5; }

};

int main()

{

MyLinSearch pr;

PairEnumerator t("inp.txt");

pr.AddEnumerator(&t);

pr.Run();

if (pr.Found())

cout << "A " << pr.Elem().name << " az első";

else cout << "Nincs";

cout << " név, amelyből legalább öt van. \n";

return 0;

}

Page 747: PROGRAMOZÁSEz 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

747

pairenumarator.h:

#ifndef PAIRENUMERATOR_H

#define PAIRENUMERATOR_H

#include <iostream>

#include <string>

#include <cstdlib>

#include "enumerator.hpp"

#include "seqinfileenumerator.hpp"

struct Pair{

std::string name;

int count;

};

class PairEnumerator : public Enumerator<Pair>{

protected:

SeqInFileEnumerator<std::string> *f;

Pair current;

bool end;

public:

PairEnumerator(const std::string& str){

try{ f =

Page 748: PROGRAMOZÁSEz 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

748

new SeqInFileEnumerator<std::string>(str);

}catch(SeqInFileEnumerator<std::string>::

Exceptions ex){

std::cout << "Nem létező bemeneti fájl!\n";

exit(1);

}

}

~PairEnumerator(){ delete f; }

void First() { f->First(); Next(); }

void Next();

bool End() const { return end;}

Pair Current() const { return current; }

};

#endif

Page 749: PROGRAMOZÁSEz 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

749

pairenumarator.cpp:

#include "pairenumerator.h"

#include "counting.hpp"

using namespace std;

class NameCounting : public Counting<string>{

public:

NameCounting(const string &str):

Counting<string >(), name(str){}

protected:

string name;

bool WhileCond(const string& e) const

{ return e == name; }

void First(){}

};

void PairEnumerator::Next(){

if(end = f->End()) return;

current.name = f->Current();

NameCounting pr(current.name);

pr.AddEnumerator(f);

pr.Run();

current.count = pr.Result();

Page 750: PROGRAMOZÁSEz 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

750

}

Page 751: PROGRAMOZÁSEz 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

751

41. Feladat: Leghosszabb szó W betűvel

Adott egy szöveges állománybeli szöveg, ahol a szavakat szóközök, tabulátor-

jelek, sorvége-jelek illetve a fájlvége-jel határolhatja. Melyik a leghosszabb

’W’ betűt tartalmazó szó?

Megoldási terv

A megoldásnál kihasználhatnánk, hogy a C++ nyelv lehetőséget ad

közvetlenül a szavak olvasására, és ekkor elég lenni minden kiolvasott szóban

’W’ betűt keresni. Ekkor azonban sok betűt kétszer is megvizsgálna a

feldolgozás: először amikor a betűt tartalmazó szót kiolvassuk, másodszor,

amikor megvizsgáljuk, hogy ’W’ betű-e. Inkább olyan megoldást készítünk,

amely karakterenként olvassa a szöveget, és minden betűt egyszer vizsgál

csak meg. Ehhez érdemes elképzelni egy olyan absztrakt felsorolót, amely a

karakterenkénti olvasást elrejti, és a szöveg szavait képes felsorolni úgy, hogy

minden szót megjelöl aszerint, hogy van-e benne ’W’ betű vagy sem. A

feladatot egy ilyen felsorolóra épített feltételes maximumkereséssel oldjuk

meg.

A = ( t : enor(Szó), l : 𝕃, szó : String )

Szó = rec(str : String , vanW : 𝕃 )

Ef = ( t=t’)

Uf = ( l, max, szó = stre

vanWet'e

.

.min

)

A t felsorolót a szöveg karakterenkénti f felsorolója, a felsorolás során

utoljára beolvasott szó (akt) és a vége logikai érték (hamis, ha még van a

felsorolásban feldolgozatlan szó) reprezentálja. A felsorolás műveleteinek

implementációja az alábbi lesz.

enor(Szó) First() Next() End() Current()

f : enor(𝕂) f.First() vége:=f.End() vége akt

Page 752: PROGRAMOZÁSEz 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

752

akt : Szó

vége : 𝕃

szó_eleje(f)

Next()

ha vége akkor

W_keres(f,akt)

szó_vége(f,akt)

szó_eleje(f)

A t.First() abban különbözik a t.Next()-től, hogy először megkersi a

legelső szó elejét. Ehhez egy előreolvasásra (f.First()) és soron következő szó

elejének megkeresésére (szó_eleje(f)) van szükség. Ez utóbbi egy kiválasztás,

amely vagy az első szó első karakterének beolvasásakor áll le, vagy ha a

szöveg végére ér (f.End()). Ha van következő szó (vége változó értéke hamis),

akkor a már megkezdett felsorolást folytatva a W_keres(f,akt) egy ’W’ betűt

keres az aktuális szóban, és a keresés során az akt.str-be gyűjti megvizsgált

betűket. Éppen ezért ez nem lineáris keresés lesz, hanem egy feltétel

fennállásáig tartó (amíg az aktuális karakter ’W’ vagy elválasztójel nem lesz)

összegzés, pontosabban másolás. Ennek elején nem szabad az f felsorolóra

meghívni annak First() műveletét, hiszen a korábban megkezdett felsorló

f.Current() művelete már az aktuális szó első betűjét mutatja. Amikor ez a

másolás ’W’ betű megtalálásával áll le, akkor az akt.vanW-t igazra, különben

hamisra kell állítani. Ezt követően meg kell még keresni a szó végét

(szó_vége(f,akt)) és az ennek során vizsgált betűket hozzá kell fűzni az

akt.str-hez. Ez is egy feltétel fennállásáig tartó (amíg az aktuális karakter

elválasztójel vagy a szöveg vége nem lesz) összegzés, amelynek az elején

nincs előreolvasás. Végül meg kell keresni a következő szó elejét

(szó_eleje(f)). Látható, hogy ezek a tevékenységek egymásnak adják át az f

felsorolót, és annak segítségével mindegyik egy kicsit tovább olvas a

szövegből.

Formálisan:

szó_eleje(t) ~ f := jelelválasztóefe

'select

Page 753: PROGRAMOZÁSEz 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

753

W_keres(f,akt) ~ akt.str,f :=

e

jelelválasztóWe

fe

}{

'

akt.vanW := e=’W’

szó_vége(f,akt) ~ akt.str,f :=

e

jelelválasztóe

fe '

Page 754: PROGRAMOZÁSEz 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

754

Implementálás

Mindenekelőtt definiáljuk a logikai értékkel megjelölt szavak típusát.

struct Word{

string str;

bool hasW;

};

A feladatot megoldó feltételes maximumkeresést (MyMaxSearch) az

általános maximumkeresésből származtatjuk. Az Item sablon-paraméter a

Word lesz (jelölt szó), a ResultType az int (a szó hossza), a harmadik

paramétert alapértelmezett értékén hagyjuk (mert maximumot keresünk). A

Func() egy szónak a hosszát adja vissza az összehasonlítások számára, a

Cond() az igaz értékkel jelölt (’W’-t tartalmazó) szavakra ad igazat.

class MyMaxSearch : public MaxSearch<Word,int> {

protected:

int Func(const Word& e) const

{ return e.str.size();}

bool Cond(const Word& e) const{ return e.hasW;}

};

A főprogram main függvénye hasonló az előző feladatokéhoz.

MyMaxSearch pr;

WordEnumerator t("inp.txt");

pr.AddEnumerator(&t);

Page 755: PROGRAMOZÁSEz 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

755

pr.Run();

if (pr.Found())

cout << "A leghosszabb W-t tartalmazó szó "

<< pr.OptElem().str << endl;

else

cout << "Nincs W-t tartalmazó szó\n";

A megoldási tervben bevezetett absztrakt felsoroló osztályát nekünk

kell definiálni. Ennek reprezentálásához a szöveget karakterenként felsoroló

szekvenciális inputfájl-felsorolót használunk, amit a konstruktor hoz létre és

a destruktor szüntet meg.

class WordEnumerator : public Enumerator<Word>{

protected:

SeqInFileEnumerator<char> *f;

Word current;

bool end;

public:

WordEnumerator(const std::string& str){

try{ f = new SeqInFileEnumerator<char>(str); }

catch(

SeqInFileEnumerator<char>::Exceptions ex){

std::cout <<"Nem létező bemeneti fájl!\n";

exit(1);

Page 756: PROGRAMOZÁSEz 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

756

}

}

~WordEnumerator(){ delete f;}

void First();

void Next();

bool End() const { return end;}

Word Current() const { return current; }

};

Látható, hogy ez az osztály a Word-del való példányosítás után az

Enumerator osztályból származik, megvalósítja a felsoroló műveleteket, a

védett adattagok pedig a tervezésnél megállapított módon reprezentálják az

absztrakt felsorolót.

A szóolvasást végző Next() metódushoz szükség van a következő szó

elejét megtaláló (SearchNextWord), az adott szóban ’W’-t kereső, de

közben a szó betűit összegyűjtő (SearchWInWord) és a szó végét kereső, de

közben a szó betűit összegyűjtő (SerachEndOfWord) tevékenységekre. Az

első tevékenység osztályát a Selection osztály-sablonból (ennek csak az f

felsoroló léptetése a feladata), a másik kettőt a Summation osztály-

sablonból származtatjuk, ahol az aktuális szó karaktereit a Result()

metódussal kérdezzük le.

Page 757: PROGRAMOZÁSEz 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

757

void WordEnumerator::Next(){

if(end = f->End()) return;

SearchWInWord pr1;

pr1.AddEnumerator(f);

pr1.Run();

current.hasW = f->Current() == 'W';

current.str = pr1.Result();

SearchEndOfWord pr2;

pr2.AddEnumerator(f);

pr2.Run();

current.str += pr2.Result();

SearchNextWord pr3;

pr3.AddEnumerator(f);

pr3.Run();

}

A szóolvasás First() metódusa a legelső szó elejének megkeresése

után a Next() metódussal azonos módon működik. A legelső szó elejét

pedig ugyanúgy keressük meg, mint bármelyik következő szó elejét

(SearchNextWord), csak nem szabad megfeledkezni arról, hogy a

Page 758: PROGRAMOZÁSEz 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

758

legeslegelső szó elejének keresése előtt el kell indítani a karakterenként

felsorolót (f->First()). Ezt mutatja az alábbi kód.

void WordEnumerator::First(){

f->First();

SearchNextWord pr;

pr.AddEnumerator(f);

pr.Run();

Next();

}

Definiáljuk végül a SearchNextWord, SearchWInWord és

SerachEndOfWord tevékenységeket. Ezek a megfelelő programozási

tételeket leíró osztályokból származnak. Mindhárom osztályban üres

utasítással definiáljuk felül a Procedure ősosztály First() metódusát,

hiszen egyik esetben sem szabad a karakterenkénti felsorolást újrakezdeni. A

kiválasztáshoz elég a biztosan bekövetkező keresési feltételt megadni

(szöveg végére érünk vagy szó elejét találunk). Az összegzésből származtatott

tevékenységeknél a ResultType paraméter a string, hiszen itt egy feltétel

fennállásáig tartó összefűzésről van szó. Az Add() végzi a hozzáfűzést, amely

vagy a szöveg végéig, vagy a WhileCond()-ban megadott feltétel

fennállásáig tart.

class SearchNextWord : public Selection<char>{

protected:

bool Cond(const char& e) const {

return Procedure<char>::enor->End() ||

Page 759: PROGRAMOZÁSEz 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

759

(e != ' ' && e != '\t'

&& e != '\r' && e != '\n');

}

void First(){}

};

class SearchWInWord

: public Summation<char, string>{

protected:

void Add(const char& e) { *result += e;}

void Init(){ *result = "";}

bool WhileCond(const char& e) const {

return e != 'W' && e != ' ' && e != '\t'

&& e != '\r' && e != '\n';

}

void First(){}

};

class SearchEndOfWord

: public Summation<char, string>{

protected:

void Add(const char& e) { *result += e;}

void Init(){ *result = "";}

bool WhileCond(const char& e) const {

Page 760: PROGRAMOZÁSEz 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

760

return e != ' ' && e != '\t'

&& e != '\r' && e != '\n';

}

void First(){}

};

Page 761: PROGRAMOZÁSEz 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

761

Tesztelés

Csak a saját kódot teszteljük fekete doboz tesztesetekkel (az alkalmazott

programozási tételek alapján):

Érvényes adatok:

1. Üres inputfájl.

2. Nincs ’W’ betűs szó, de van több nem ’W’ betűs.

3. Egyetlen ’W’ betűs szó van és sok másik.

4. Több ’W’ betűs szó, köztük a leghosszabból is több.

5. Közvetlenül egymás után több ’W’ betűs szó.

6. Közvetlenül egymás után több nem ’W’ betűs szó.

7. Váltakozva ’W’ betűs és nem ’W’ betűs szavak.

8. Állomány elején elválasztó jelek.

9. Állomány végén elválasztó jelek.

10. Elválasztó jelek vegyesen, közvetlenül egymás után is.

11. ’W’ betű a szó elején.

12. ’W’ betű a szó közepén.

13. ’W’ betű a szó végén.

14. Több ’W’ betű a szóban.

Érvénytelen adatok:

1. Nem létező állománynév.

Page 762: PROGRAMOZÁSEz 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

762

Teljes program

A saját kód több forrásállományban helyezkedik el. Külön fordítási egységet

alkot a szavak felsorolását definiáló osztály, és az ahhoz szükséges egyéb

struktúrák és osztályok.

wordenumarator.h:

#ifndef WORDENUMERATOR_H

#define WORDENUMERATOR_H

#include <iostream>

#include <string>

#include <cstdlib>

#include "enumerator.hpp"

#include "seqinfileenumerator.hpp"

struct Word{

std::string str;

bool hasW;

};

class WordEnumerator : public Enumerator<Word>{

protected:

SeqInFileEnumerator<char> *f;

Word current;

bool end;

Page 763: PROGRAMOZÁSEz 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

763

public:

WordEnumerator(const std::string& str){

try{ f = new SeqInFileEnumerator<char>(str); }

catch(

SeqInFileEnumerator<char>::Exceptions ex){

std::cout <<"Nem létező bemeneti fájl!\n";

exit(1);

}

}

~WordEnumerator(){ delete f;}

void First();

void Next();

bool End() const { return end;}

Word Current() const { return current; }

};

#endif

Page 764: PROGRAMOZÁSEz 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

764

wordenumarator.cpp:

#include "wordenumerator.h"

#include "selection.hpp"

#include "summation.hpp"

using namespace std;

class SearchNextWord : public Selection<char>{

protected:

bool Cond(const char& e) const {

return Procedure<char>::enor->End() ||

(e != ' ' && e != '\t'

&& e != '\r' && e != '\n');

}

void First(){}

};

class SearchWInWord

: public Summation<char, string>{

protected:

void Add(const char& e) { *result += e;}

void Init(){ *result = "";}

bool WhileCond(const char& e) const {

return e != 'W' && e != ' ' && e != '\t'

Page 765: PROGRAMOZÁSEz 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

765

&& e != '\r' && e != '\n';

}

void First(){}

};

class SearchEndOfWord

: public Summation<char, string>{

protected:

void Add(const char& e) { *result += e;}

void Init(){ *result = "";}

bool WhileCond(const char& e) const {

return e != ' ' && e != '\t'

&& e != '\r' && e != '\n';

}

void First(){}

};

void WordEnumerator::First(){

f->First();

SearchNextWord pr;

pr.AddEnumerator(f);

pr.Run();

Next();

}

void WordEnumerator::Next(){

Page 766: PROGRAMOZÁSEz 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

766

if(end = f->End()) return;

SearchWInWord pr1;

pr1.AddEnumerator(f);

pr1.Run();

current.hasW = f->Current() == 'W';

current.str = pr1.Result();

SearchEndOfWord pr2;

pr2.AddEnumerator(f);

pr2.Run();

current.str += pr2.Result();

SearchNextWord pr3;

pr3.AddEnumerator(f);

pr3.Run();

}

main.cpp:

#include <iostream>

#include "wordenumerator.h"

#include "maxsearch.hpp"

using namespace std;

Page 767: PROGRAMOZÁSEz 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

767

class MyMaxSearch : public MaxSearch<Word,int> {

protected:

int Func(const Word& e) const

{ return e.str.size();}

bool Cond(const Word& e) const{ return e.hasW;}

};

int main()

{

MyMaxSearch pr;

WordEnumerator t("inp.txt");

pr.AddEnumerator(&t);

pr.Run();

if (pr.Found())

cout << "A leghosszabb W-t tartalmazó szó "

<< pr.OptElem().str << endl;

else

cout << "Nincs W-t tartalmazó szó\n";

return 0;

}

42. Feladat: Összefuttatás

Adott két szöveges állomány, bennük egész számok szigorúan növekedően

rendezve. Gyűjtsük ki az összes számot, de mindegyiket csak egyszer egy

harmadik szöveges állományba!

Page 768: PROGRAMOZÁSEz 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

768

Megoldási terv

A megoldás egy úgynevezett összefuttatás, pontosabban az elemek uniózása.

Ez egy olyan másolás, amelyik számára egy felsoroló a két állomány összes

elemét előállítja, de mindegyiket csak egyszer. A megoldáshoz elképzeljük azt

a felsorolót, amely a már összefuttatott számsorozatot járja be, hogy a

segítségével az elemeket átmásoljuk egy szekvenciális outputfájlba.

A = ( t : enor(ℤ ), z : seq(ℤ) )

Ef = ( t=t’ )

Uf = ( z = t’ )

Természetesen a t felsoroló megvalósításának hátterében a bemenő

adatként szereplő szöveges állományoknak, mint szekvenciális inputfájloknak

az és x és y felsorolói állnak.

enor(ℤ) First() Next() End() Current()

x : enor(ℤ)

y : enor(ℤ)

akt : ℤ

vége : 𝕃

x.First()

y.First()

Next()

lásd alább

vége akt

A Next() művelet az x és y szimultán felsorolásaira épül, értéket ad a

vége logikai változónak és az akt változónak. Kihasználjuk, hogy mindkét

felsorolás szigorúan növekedő sorrendben járja be a szöveges

állományokban rögzített számokat. Ezért ha a két felsoroló még nem ért

véget és az aktuális (Current()) elemeik között van kisebb, akkor a kisebbik

biztosan nem szerepelhet a másik felsorolásában. Ha az aktuális elemek

egyenlők, akkor az mindkét felsorolásban megtalálható. Ezen kívül azokra az

Page 769: PROGRAMOZÁSEz 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

769

esetre is gondolni kell, amikor valamelyik felsorolás már befejeződött.

Összességében egyértelműen értékül adható az akt változónak az a szám,

amely a két felsorolásból származó soron következő elem lesz. Ha valamelyik

felsoroló által kijelölt aktuális elemet feldolgoztuk, akkor azzal a felsorolóval

tovább kell lépni. Az itt alkalmazott technikának következménye az is, hogy

az összefuttatott felsorolás is szigorúan rendezett számsorozatot állít elő.

Next()

vége := x.End() y.End()

vége

y.End() (x.End()

x.Current()<

y.Current())

x.End() (y.End()

x.Current()>

y.Current())

x.End() y.End()

x.Current()=

y.Current()

akt:=x.Current() akt:=y.Current() akt:=x.Current()

x.Next(); y.Next() x.Next(); y.Next()

Implementálás

Két tevékenységet kell definiálnunk: a felsorolt elemek outputfájlba kiíró

másolását és az inputfájlok elemeinek összefuttató felsorolását.

A fő tevékenység – amelyik végső soron az uniózást végzi – a felsorolt

egész számoknak egy szekvenciális outputfájlba történő átmásolása, amely

speciális összegzésként fogható fel. Ez a Summation osztálynak egy

egyszerűbb felhasználása annál, mint amit a 38. feladat kiválogatásánál

láttunk. Az Item sablon-paraméter itt az int lesz, a ResultType pedig az.

class Union : public Summation<int,ostream >{

protected:

void Add(const int& e){*result << e << endl;}

Page 770: PROGRAMOZÁSEz 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

770

public:

Union(ofstream *f):Summation<int,ofstream>(f){}

};

Az osztály konstruktora kapja majd meg annak az ostream típusú

kimeneti adatfolyamnak a címét, ahová a számokat ki kell másolni. A másolás

egy lépését, egy szám egy sorba való kiírását az Add() metódus végzi.

Az összefuttatást végző felsoroló a két bemeneti szöveges állomány

elemeit felsoroló (x, y) objektumok szimultán használatára épül. Ezek

szekvenciális inputfájlként kezelik a szöveges állományokat, az összefuttató

felsoroló konstruktora hozza létre és a destruktora szünteti meg őket. A

konstruktor kezeli a szöveges állományok megnyitásával kapcsolatos kivételt

is. Az End() metódus az összefuttató felsorolás végét jelző logikai adattag

(end) értékét, a Current() metódus az éppen felsorolandó számot

tartalmazó adattag (current) értékét adja vissza. A First() metódus

minkét szöveges állományból olvas egy számot, majd meghívja a Next()

metódust, amely a felsorolás egy lépését végzési el, azaz a soron következő

számot (current) állítja elő.

class CombineEnumerator : public Enumerator<int>{

protected:

SeqInFileEnumerator<int> *x;

SeqInFileEnumerator<int> *y;

int current;

bool end;

public:

CombineEnumerator(const std::string& str1,

const std::string& str2){

Page 771: PROGRAMOZÁSEz 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

771

try{

x = new SeqInFileEnumerator<int>(str1);

y = new SeqInFileEnumerator<int>(str2);

}catch

(SeqInFileEnumerator<int>::Exceptions ex){

std::cout <<"Nem létező bemeneti fájl!\n";

exit(1);

}

}

~CombineEnumerator(){ delete x; delete y; }

void First() { x->First(); y->First(); Next();}

void Next();

bool End() const { return end; }

int Current() const { return current;}

};

A Next() mindenekelőtt megvizsgálja, hogy van-e még felsorolandó

elem. Ha nincs (mert a szöveges állományok számainak felsorolása véget

ért), akkor az end adattagot igazra állítja: ez jelzi a felsorolás végét.

Egyébként pedig eldönti, hogy melyik a soron következő felsorolandó elem,

és azt elhelyezi az current adattagban.

void CombineEnumerator::Next() {

if( end = x->End() && y->End()) return;

if( y->End() || (!x->End()

Page 772: PROGRAMOZÁSEz 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

772

&& x->Current() < y->Current())){

current = x->Current();

x->Next();

}else if( x->End() || (!y->End()

&& x->Current() > y->Current())){

current = y->Current();

y->Next();

}else if(!x->End()&& !y->End()

&& x->Current() == y->Current()){

current = x->Current();

x->Next(); y->Next();

}

}

Végül nézzük meg a main függvényt.

ofstream u("out.txt");

if(u.fail()) exit(2);

CombineEnumerator t("inp1.txt","inp2.txt");

Union pr(&u);

pr.AddEnumerator(&t);

pr.Run();

Page 773: PROGRAMOZÁSEz 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

773

Az írásra megnyitott szöveges állományra irányított adatfolyam (u) a

másolás eredményváltozója, ennek a címét adjuk az Union típusú

tevékenység objektumnak. A felsoroló objektum a két bemeneti szöveges

állomány szimultán olvasására épülő CombineEnumerator típusú lesz.

Page 774: PROGRAMOZÁSEz 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

774

Tesztelés

Csak a saját kódot teszteljük.

Fekete doboz tesztesetek: (az összegzés programozási tétele alapján)

Érvényes adatok:

1. Mindkét inputfájl üres.

2. Egyik inputfájl üres, a másik nem.

3. Olyan elem, amelyik csak az egyik, csak a másik, illetve mindkettő

inputfájlban szerepel.

Érvénytelen adatok:

1. Nem létező állomány nevek

Fehér doboz tesztesetek:

Mondjuk 1-es típusúnak azt az elemet, amelyik csak az első állományban

szerepel, 2-es típusúnak, amelyik csak a másodikban, 3-as típusúban, amelyik

mindkettőben.

1. Az elemek felsorolásában egymást követő három elem típusa az

1,2,3 összes permutációja.

2. Több 1-es (2-es illetve 3-as) típusú elem után jön több másik típusú.

Page 775: PROGRAMOZÁSEz 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

775

Kiegészítés

Érdemes végiggondolni, hogyha a fenti feladatban csak a bemeneti

állományok közös elemeit kellene az összefuttatást végző felsorolónak

bejárni (mert a két állomány elemeinek metszetét kell előállítani), akkor

hogyan módosulna az absztrakt felsoroló Next() műveletét megvalósító

program. (Minden más változatlan marad.)

void CombineEnumerator::Next(){

if ( end = x->End() || y->End() ) return;

if( x->Current() < y->Current() ){

x->Next();

Next();

}else if( x->Current() > y->Current() ){

y->Next();

Next();

}else if( x->Current() == y->Current() ){

current = x->Current();

x->Next(); y->Next();

}

}

Az egyik ilyen változás, hogy jelentősen egyszerűsödnek a hármas

elágazás feltételei annak köszönhetően, hogy csak akkor adódik erre a

vezérlés, ha még sem az x, sem az y felsorolás nem ért véget.

A másik módosítást az a probléma szülte, hogy itt a hármas elágazás

első két ága nem állít elő újabb felsorolandó elemet a Current() számára,

de ugyanakkor a Next() metódus nem fejeződhet be addig, amíg ilyet nem

Page 776: PROGRAMOZÁSEz 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

776

talál (vagy nem ér véget a felsorolás). Ezt az ellentmondást kétféleképpen

oldhatjuk fel. Megtehetjük, hogy az első két ág egy-egy rekurzív hívással újra

elindítja a Next()-et. Így amíg nem találunk az x és y felsorolásokban közös

elemet (vagy az egyik nem ér véget), addig nem ér véget Next() metódus

sem. Ezt a változatot látjuk fenn. A másik megoldás az lehetne, hogy az itt

megvalósított felsoroló egy úgynevezett „üres” elemet is vissza tudna adni,

amelyet majd annak a feldolgozásnak kell lekezelni, azaz figyelmen kívül

hagyni, amelyik számára a felsoroló az elemeket szolgáltatja. Az üres elem

egy olyan értékű elem, amely biztosan nem fordulhat el a normális elemek

esetén vagy az elem tartalmazhat egy logikai komponens, amely az elem

normális vagy üres állapotára utal.

Page 777: PROGRAMOZÁSEz 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

777

Teljes program

combineenumerator.h:

#ifndef COMBINEENUMERATOR_H

#define COMBINEENUMERATOR_H

#include <iostream>

#include <string>

#include <cstdlib>

#include "enumerator.hpp"

#include "seqinfileenumerator.hpp"

class CombineEnumerator : public Enumerator<int>{

protected:

SeqInFileEnumerator<int> *x;

SeqInFileEnumerator<int> *y;

int current;

bool end;

public:

CombineEnumerator(const std::string& str1,

const std::string& str2){

try{

x = new SeqInFileEnumerator<int>(str1);

y = new SeqInFileEnumerator<int>(str2);

Page 778: PROGRAMOZÁSEz 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

778

}catch

(SeqInFileEnumerator<int>::Exceptions ex){

std::cout <<"Nem létező bemeneti fájl!\n";

exit(1);

}

}

~CombineEnumerator(){ delete x; delete y; }

void First() { x->First(); y->First(); Next();}

void Next();

bool End() const { return end; }

int Current() const { return current;}

};

#endif

Page 779: PROGRAMOZÁSEz 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

779

combineenumerator.cpp:

#include "combineenumerator.h"

using namespace std;

void CombineEnumerator::Next() {

if( end = x->End() && y->End()) return;

if( y->End() || (!x->End()

&& x->Current() < y->Current())){

current = x->Current();

x->Next();

}else if( x->End() || (!y->End()

&& x->Current() > y->Current())){

current = y->Current();

y->Next();

}else if(!x->End()&& !y->End()

&& x->Current() == y->Current()){

current = x->Current();

x->Next(); y->Next();

}

}

main.cpp:

#include <fstream>

Page 780: PROGRAMOZÁSEz 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

780

#include <cstdlib>

#include "combineenumerator.h"

#include "summation.hpp"

using namespace std;

class Union : public Summation<int,ofstream >{

protected:

void Add(const int& e){*result << e << endl;}

public:

Union(ofstream *f):Summation<int,ofstream>(f){}

};

int main()

{

ofstream u("out.txt"); if(u.fail()) exit(2);

CombineEnumerator t("inp1.txt","inp2.txt");

Union pr(&u);

pr.AddEnumerator(&t);

pr.Run();

return 0;

}

Page 781: PROGRAMOZÁSEz 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

781

IRODALOM JEGYZÉK

1. Alexandrescu, A., Sutter H.: „C++ Coding Standards: 101 Rules,

Guidelines, and Best Practices.” 1st Edition, Pearson Education In.,

Addison Wesley Professional, 2005, ISBN-0-32-111358-6.

2. Angster, E.: „Objektumorientált tervezés és programozás”, 4KÖR Bt,

2003, ISBN-9-63-006263-1.

3. Beck, K.: „Implementation Patterns.” 1st Edition, ISBN 0321413091,

Pearson Education In., Addison Wesley Professional, 2008, ISBN-0-

32-141309-1.

4. Booch G., Rumbaugh J., Jakobson I.: „The Unified Modeling Language

User Guide (Second Edition)”, Addison-Wesley, 2005, ISBN-0-32-

126797-4.

5. Cormen, T. H., Leiserson, C. E.,Rivest, R. L., Stein C.: „Új algoritmusok,

Scolar Informatika”, 2003, ISBN-9-63-919390-9.

6. Gamma E., Helm R., Johnson R., Vlissides J.: „Design Patterns --

Elements of Reusable Object-Oriented Software”, Addison-Wesley

Longman, 1995, ISBN-0-47-129551-5.

7. Meyer B.: „Object Oriented Software Construction”, Prentice Hall,

1997, ISBN-0-13-629155-4.

8. Sharp, J.: „Visual C# 2005 lépésről lépésre”, SZAK Kiadó Kft, 2005,

ISBN-9-63-913179-2.

9. Stroustrup, B.: „A C++ programozási nyelv”, Kiskapu Kft, 2001, ISBN-

9-63-930118-3.

10. Rumbaugh J. et al.: „Object Oriented Modeling and Design”, Prentice

Hall, 1991, ISBN-0-13-630054-5.