You are on page 1of 781

Gregorics Tibor

PROGRAMOZÁS

2. kötet

MEGVALÓSÍTÁS

1
Egyetemi jegyzet

2012

2
ELŐSZÓ .................................................................................................................... 8

BEVEZETÉS ............................................................................................................. 10

I. RÉSZ ALAPOK ...................................................................................................... 17

1. ELSŐ LÉPÉSEK ......................................................................................................... 20

Implementációs stratégia .............................................................. 20


Nyelvi elemek ................................................................................ 23
1. Feladat: Osztási maradék .......................................................... 28
2. Feladat: Változó csere ............................................................... 39
C++ kislexikon ................................................................................ 44
2. STRUKTURÁLT PROGRAMOK ...................................................................................... 45

Implementációs stratégia .............................................................. 45


Nyelvi elemek ................................................................................ 46
3. Feladat: Másodfokú egyenlet .................................................... 61
4. Feladat: Legnagyobb közös osztó .............................................. 73
5. Feladat: Legnagyobb közös osztó még egyszer ......................... 80
C++ kislexikon ................................................................................ 86
3. TÖMBÖK ............................................................................................................... 88

Implementációs stratégia .............................................................. 88


Nyelvi elemek ................................................................................ 90
6. Feladat: Tömb maximális eleme ................................................ 96
7. Feladat: Mátrix maximális eleme ............................................ 111
8. Feladat: Melyik szóra gondoltam ............................................ 117
C++ kislexikon .............................................................................. 127
4. KONZOLOS BE- ÉS KIMENET ..................................................................................... 129

Implementációs stratégia ............................................................ 129


Nyelvi elemek .............................................................................. 132

3
9. Feladat: Duna vízállása ............................................................ 142
10. Feladat: Alsóháromszög-mátrix............................................. 150
C++ kislexikon .............................................................................. 163
5. SZÖVEGES ÁLLOMÁNYOK......................................................................................... 167

Implementációs stratégia ............................................................ 167


Nyelvi elemek .............................................................................. 170
11. Feladat: Szöveges állomány maximális eleme....................... 179
12. Feladat: Jó tanulók kiválogatása............................................ 187
C++ kislexikon .............................................................................. 194
II. RÉSZ PROCEDURÁLIS PROGRAMOZÁS.............................................................. 199

6. ALPROGRAMOK A KÓDBAN ...................................................................................... 202

Implementációs stratégia ............................................................ 202


Nyelvi elemek .............................................................................. 205
13. Feladat: Faktoriális ................................................................ 211
14. Feladat: Adott számmal osztható számok ............................. 218
15. Feladat: Páros számok darabszáma....................................... 230
C++ kislexikon .............................................................................. 236
7. PROGRAMOZÁSI TÉTELEK IMPLEMENTÁLÁSA ............................................................... 239

Implementációs stratégia ............................................................ 239


Nyelvi elemek .............................................................................. 242
16. Feladat: Legnagyobb osztó .................................................... 247
17. Feladat: Legkisebb adott tulajdonságú elem......................... 262
18. Feladat: Keressünk Ibolyát .................................................... 272
C++ kislexikon .............................................................................. 282
8. TÖBBSZÖRÖS VISSZAVEZETÉS ALPROGRAMOKKAL ......................................................... 285

Implementációs stratégia ............................................................ 286

4
Nyelvi elemek .............................................................................. 290
19. Feladat: Kitűnő tanuló ........................................................... 292
20. Feladat: Azonos színű oldalak................................................ 304
21. Feladat: Mátrix párhozamos átlói ......................................... 315
C++ kislexikon .............................................................................. 330
9. FORDÍTÁSI EGYSÉGEKRE BONTOTT PROGRAM .............................................................. 331

Implementációs stratégia ............................................................ 332


Nyelvi elemek .............................................................................. 334
22. Feladat: Műkorcsolya verseny ............................................... 337
23. Feladat: Melyikből hány van.................................................. 365
C++ kislexikon .............................................................................. 374
10. REKURZÍV PROGRAMOK KÓDOLÁSA ......................................................................... 376

Implementációs stratégia ............................................................ 376


Nyelvi elemek .............................................................................. 378
24. Feladat: Binomiális együttható.............................................. 380
25. Feladat: Hanoi tornyai ........................................................... 390
26. Feladat: Quick sort................................................................. 397
III. RÉSZ PROGRAMOZÁS OSZTÁLYOKKAL ............................................................ 409

11. A TÍPUS MEGVALÓSÍTÁS ESZKÖZE: AZ OSZTÁLY ........................................................... 411

Implementációs stratégia ............................................................ 411


Nyelvi háttér ................................................................................ 414
27. Feladat: UFO-k ....................................................................... 423
28. Feladat: Zsák .......................................................................... 439
29. Feladat: Síkvektorok .............................................................. 450
C++ kislexikon .............................................................................. 465
12. FELSOROLÓK TÍPUSAINAK MEGVALÓSÍTÁSA ............................................................... 467

5
Implementációs stratégia ............................................................ 467
Nyelvi háttér ................................................................................ 470
30. Feladat: Könyvtár................................................................... 476
31. Feladat: Havi átlag-hőmérséklet............................................ 498
32. Feladat: Bekezdések .............................................................. 520
C++ kislexikon .............................................................................. 539
13. DINAMIKUS SZERKEZETŰ TÍPUSOK OSZTÁLYAI ............................................................ 541

Implementációs stratégia ............................................................ 542


Nyelvi elemek .............................................................................. 545
33. Feladat: Verem ...................................................................... 556
34. Feladat: Kettős sor ................................................................. 586
C++ kislexikon .............................................................................. 610
14. OBJEKTUM-ORIENTÁLT KÓD-ÚJRAFELHASZNÁLÁSI TECHNIKÁK ....................................... 612

Implementációs stratégia ............................................................ 613


Nyelvi elemek .............................................................................. 618
35. Feladat: Túlélési verseny ....................................................... 622
36. Feladat: Lengyel forma és kiértékelése ................................. 642
37. Feladat: Bináris fa bejárása ................................................... 677
C++ kislexikon .............................................................................. 705
15. EGY OSZTÁLY-SABLON KÖNYVTÁR FELHASZNÁLÁSA ..................................................... 707

Osztály-sablon könyvtár tervezése.............................................. 708


Osztály-sablon könyvtár implementálása ................................... 717
38. Feladat: Kiválogatás ............................................................... 729
39. Feladat: Feltételes maximumkeresés .................................... 734
40. Feladat: Keresés .................................................................... 739
41. Feladat: Leghosszabb szó W betűvel ..................................... 751

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

7
ELŐSZÓ

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


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

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

9
BEVEZETÉS

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


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

Feladat

Elemzés

Specifikáció

Tervezés

Programterv

Megvalósítás

Programkód

Tesztelés, Dokumentálás

Megoldás

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

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

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

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

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

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

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

16
I. RÉSZ
ALAPOK

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


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

17
1. A programkód szerkezete
2. Típusok, kifejezések, változók, értékadás
3. Vezérlési szerkezetek
4. Input-output
5. Könyvtári elemek

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

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


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

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

19
1. Első lépések

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


feladatok megoldásán keresztül mutatjuk be a programkészítésnek azt a
szakaszát, amelyet megvalósításnak vagy más néven implementálásnak
neveznek. Ezzel párhuzamosan megismerkedünk a C++ programozási nyelv
néhány alapvető nyelvi elemével: az egész számok típusával, változók
deklarálásának módjával, az értékadás utasítással és a szabványos 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. Programkód keretének elkészítése


2. Absztrakt program kódolása
3. Bemenő adatok beolvasása és ellenőrzése
4. Eredmény megjelenítése
5. Változók deklarálása

1-1. ábra. Egyszerű programok implementálásának lépései

Egy program kódját a választott programozási nyelvre jellemző keretbe


kell befoglalni. Ehhez a programozási nyelv azon speciális nyelvi elemeit,
utasításait kell felhasználni, amelyek kijelölik a programkód elejét és végét,
meghatározzák, hogy melyik utasítás végrehajtásával kezdődjön a program

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

21
stratégia közül választhatunk. Az egyik megfelelő hibaüzenet generálása után
leállítja a program futását, a másik újra és újra megpróbálja beolvasni az
adatot, amíg az helyes nem lesz. Azt, hogy ezek közül melyiket alkalmazzuk, a
konkrét feladat kitűzése illetve a beolvasandó adat forrása határozza meg.
Például, ha az adatokat közvetlenül a felhasználótól kapjuk, akkor
alkalmazható a második módszer, de ha az adatok egy korábban feltöltött
szöveges állományból származnak, akkor csak az első út járható.
A kiírás figyelembe veszi az eredmény-változók típusát (ez az
állapottérből olvasható ki), valamint a specifikáció utófeltételét. Gyakori eset
például az, hogy egy eredmény-változó csak bizonyos feltétel teljesülése
esetén kap értéket. Ilyenkor a kiírás kódja egy elágazást fog tartalmazni,
amely csak megadott esetben írja ki az eredményt, egyébként pedig egy
tájékoztatást ad annak okáról, hogy az eredmény miért nem jelenhet meg.

Bemenő változó: Az absztrakt program olyan változója, amely


az absztrakt program végrehajtása előtt rendelkezik (a
megoldandó feladat előfeltételének) megfelelő
kezdőértékkel.
Eredmény-változó: Az absztrakt program olyan változója,
amely az absztrakt program (tervezés által garantált)
befejeződése után a (megoldandó feladat utófeltétele által)
kívánt értéket tartalmazza.
Segédváltozó: A program működése közben létrejött változó.

1-2. ábra. Program változói tervezési szempontból

A változók fontos szereplői a programoknak. Ezek java részét a


tervezés során vezetjük be, hiszen már ott kiderülnek a megoldandó feladat
bemenő- és eredmény-változói, valamint számos segédváltozó is, de az
implementáláskor még további segédváltozók is megjelenhetnek. A változók
bevezetése a tervezés szintjén a változók típusának megadásával jár együtt.
A programban használt változókat (a feladat bemenő- és eredmény-változóit,

22
az absztrakt program segédváltozóit, és az implementálás során bevezetett
egyéb segédváltozókat) többnyire (C++-ban mindig) deklarálni kell, azaz
explicit módon meg kell adni a típusukat. Ehhez ismerni kell az adott
nyelvben a deklaráció formáját, elhelyezésének módját, a deklarációnál
használható alaptípusokat és azok tulajdonságait (típusértékeit és
műveleteit). Vannak azonban olyan programozási nyelvek is, ahol a változó
első értékadása az értékül adott kifejezés típusával deklarálja a változó
típusát. A változók deklarációja a megvalósítás ötödik lépése.

Nyelvi elemek

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


amelyet bármilyen szöveges állományt előállító szövegszerkesztővel
elkészíthetünk. Ennek az állománynak általában valamilyen speciális, az adott
programozási nyelvre utaló kiterjesztése van. A C++ nyelvű programjainkat
„.cpp” kiterjesztésű állományokba tesszük. (A gyakorlatban ezen kívül
gyakran előfordul a .c, .C, .cxx kiterjesztés is.) Az egyszerű programok
egyetlen forrásállományban helyezkednek el, az összetett programok kódját
több állományba lehet szétosztani. A forrásállományokban elhelyezett
kódban egyértelműen kell megjelölni azt az utasítást, amelynél a program
végrehajtása (futása) elkezdődik. C++ nyelven ez az úgynevezett main
függvény blokkjának első utasítása. Ezt a blokkot a main függvény nevét
követő nyitó és csukó kapcsos-zárójelek határolják.
A kerethez tartoznak a kód olyan bejegyzései, amelyek a fordító
program számára hordoznak üzenetet. Ilyen például azon kódkönyvtárak
kijelölése, amelynek elemeit fel szeretnénk használni, ezért a kódkönyvtárat
be akarjuk másolni (include) a programunkba. A konzolos input-output
tevékenységekhez például szükségünk lesz egy #include <iostream>
sorra.
Itt kell megemlíteni a minden forrásállomány elején kötelezően
ajánlott using namespace std utasítást is. Számos sokszor használt
előredefiniált azonosító ugyanis az úgynevezett standard (std) névtérben
található. Ez azt jelenti, hogy ezek az előre definiált elemek egy olyan
csomagban találhatók, amelynek elemeire az std:: előtag (úgynevezett

23
minősítés) segítségével lehetne csak hivatkozni, ha nem használnánk a
using namespace std utasítást.
C++ nyelven a main függvényben elhelyezett return utasítás a
program futását állítja le. A main függvény előtti int (integer) szócska utal
arra, hogy a return után egy egész számot (egész értékű kifejezést) kell írni.
Ezt az értéket a program leállása után a futtató környezet (az operációs
rendszer) kapja meg, és tetszés szerint reagálhat rá. Ha ez nulla, akkor az a
„minden rendben” üzenetet szimbolizálja. A return kifejezés helyett
használható még az exit(kifejezés) is, de ehhez bizonyos
környezetekben be kell illeszteni a program elejére egy #include
<cstdlib> sort.
A változó deklarációja a változó típusát írja le. A típus határozza meg,
hogy egy változó milyen értékeket vehet fel és azokkal milyen műveletek
végezhetők. Egy futó program változójához a típusán kívül hozzátartozik a
számítógép memóriájának azon része is, ahol a változó aktuális értéke
tárolódik. A memóriára első megközelítésben gondoljunk úgy, mint bájtok (1
bájt = 8 darab 0 vagy 1-est tároló bit) sorozatára. Minden bájtot
egyértelműen azonosít az ebben a sorozatban elfoglalt pozíciója, azaz
sorszáma, amit a bájt címének hívunk. A sorszámozást nullával kezdjük.

Számok bináris alakja

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


számrendszerben) ábrázoljuk. A bináris alakban felírt szám egy számjegye 0
vagy 1 értékű lehet. Ezt hívjuk bitnek. A számjegy értéke a pozíciójától, a
helyiértéktől függ. A kettes számrendszer helyiértékei a kettő hatványai. Egy
természetes szám bináris alakjának számjegyeit helyiérték szerint növekvő
sorrendben egy kettővel való osztogatásos módszerrel lehet előállítani. Az
osztásoknál keletkezett maradék a soron következő számjegy, az osztás
eredménye pedig a következő osztás osztandója. Az osztogatás addig tart,
amíg az osztandó nem nulla.
Példa: Adjuk meg a 183(10) bináris alakját!
183 : 2 = 91 maradt: 1
91 : 2 = 45 maradt: 1

24
45 : 2 = 22 maradt: 1
22 : 2 = 11 maradt: 0
11 : 2 = 5 maradt: 1
5:2=2 maradt: 1
2:2=1 maradt: 0
1:2=0 maradt: 1
Tehát 183(10) = 10110111(2) = 1*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)

25
Egy változónak csak azt követően lehet értéket adni, hogy kijelöltük a
memóriában a helyét, azaz megtörtént a változó helyfoglalása. Ezt nevezzük
a változó definíciójának. Az értéket bináris kódolással egy vagy több egymás
utáni bájton tároljuk. A tároláshoz szükséges memória szelet első bájtjának
címe a változó címe, a tároláshoz használt bájtok száma a változó mérete.

változó regisztráció
lefoglalt memória
név típus cím
szelet
érték

1-3. ábra. Változó jellemzői

Az erősen típusos nyelvekben egy változónak mindig van típusa, de


neve, memória címe és értéke nem feltételenül. A változó deklarációja és a
helyfoglalása közötti időben a változó nem rendelkezik még memória
területtel, tehát sem címe, sem értéke nincs. A memóriafoglalás után, a
változónak már lesz értéke, mert a kijelölt memória szelet eleve tartalmaz 0
vagy 1-es biteket. Az ezekből kiszámolt érték azonban előre nem ismert,
bizonytalan, másképpen fogalmazva nem definiált, amit úgy is felfoghatunk,
hogy a változó még nem kapott kezdeti értéket. (Találkozhatunk olyan
változókkal is, amelyeknek hiányzik a neve, az értékükre csak a címük
segítségével hivatkozhatunk.)
C++ nyelven a változó neve (csak úgy, mint más azonosítóké) betűk és
számok sorozatából áll. Az első karakternek mindig betűnek vagy _ (aláhúzás)
jelnek kell lennie, és nem használhatóak változónévként a nyelv foglalt
kulcsszavai.
A változókkal kapcsolatos alapvető utasítás az értékadás. Egy kicsit
megtévesztő, hogy az absztrakt programokban (és a Pascal nyelven)
„változó:=kifejezés” formában felírt értékadást a C-szerű nyelvekben a
„változó = kifejezés” utasítás írja le, azaz nincs benne az értékadás irányát
jelző kettőspont. A kifejezés olyan változókból, típusértéket megjelenítő
szimbólumokból (úgynevezett literálokból), műveleti jelekből és függvények

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

27
1. Feladat: Osztási maradék

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


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

Specifikáció

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


értékül vevő változó szerepel, ezek között az x és y változók kezdetben
bemenő adatokat tartalmaznak, amelyek rögzített kezdő értékeit jelöljük az
x’-vel és y’-vel. Mivel az x-nek az y-nal vett osztási maradékát (x mod y) kell
kiszámolnunk, az y értéke nem lehet nulla, hiszen a nullával való osztás
eredménye nincs értelmezve.
A = ( x, y, z : ℕ )
Ef = ( x=x’ y=y’ y’>0 )
Uf = ( x=x’ y=y’ z = x mod y )

Absztrakt program

A számítások elvégzését megoldó algoritmus a célfeltételből közvetlenül


adódik. A z változónak kell új értéket adni, amely az x értékének y értékével
vett osztási maradéka.

z := x mod y

Implementálás

A programkódot a klasszikus hármas tagolással adjuk meg, amely egy jól


megírt esszé (bevezetés, tárgyalás, befejezés) programozási megfelelője
1. A bemenő adatok beolvasása
2. Számolás: Az absztrakt program kódja
3. Az eredmény kiírása

28
amelyek implementálását a korábban ismertetett öt lépésben végezzük el.
Elkészítjük a program keretét, deklaráljuk a változóit, majd kódoljuk a fenti
három részt.

Program kerete

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


blokkjában (kapcsos zárójelekkel közre zárt részén) elhelyezett utasítások
végrehajtásával kezdődik a program futása. A blokk utasításai
pontosvesszővel vagy csukó kapcsos zárójellel végződnek. Ezek egyfelől
elkülönítik az utasításokat, másfelől a közöttük levő sorrendet is
egyértelműen kijelölik.
Abban a forrásállományban (legyen ennek a neve most main.cpp),
ahol a main függvényt elhelyezzük, még a függvény előtt le kell írnunk két
sort. Az első, az
#include <iostream>

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


könyvtári csomagra hivatkozik, a második arra szolgál, hogy cin, cout
azonosítókat ne kelljen std::cin, std::cout formában írni.

#include <iostream>

using namespace std;

int main()

...

return 0;

29
A pontozott rész helyére kerülnek majd az utasítások. A return 0 a
program futását leállító utasítás. A nulla érték a „minden rendben” üzenetet
szimbolizálja. A main függvény előtti int (integer, egész szám típusú)
szócska a visszatérési érték (ami tehát most a nulla) típusát jelzi.

Deklarációk

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


önálló utasítás:
típus változó

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


típusától különböző jele. (Az előjel nélküli, unsigned, egészeket mi nem
használjuk.) Több azonos típusú változó deklarációjakor a közös típusnév
után vesszővel elválasztva is felsorolhatjuk a változó neveket.

int x, y, z;

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


alapműveletek (+, *, -, /) használatát. Az osztás itt az úgynevezett egé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.

30
A változó:=kifejezés értékadást a C++ nyelvben változó=kifejezés
formában írjuk. Az értékadás-utasítások után mindig pontosvessző áll
kivéve, ha az nem önálló utasításként, hanem egy kifejezés részeként
jelenik meg.
Az absztrakt programunk C++ nyelvű kódja tehát egyetlen értékadás
utasításból áll, amit az alábbi módon írunk le.

z = x % y;

Bemenő adatok beolvasása

A beolvasás során két problémát is meg kell oldanunk. Egyfelől el kell juttatni
a program futása során a felhasználó által begépelt számokat az x és y
változókba, másfelől ellenőrizni kell, hogy ezek a számok a feladat
szempontjából helyesek-e. Ez utóbbi valójában három féle ellenőrzést jelent.
Egyrészt vizsgálni lehetne, hogy a felhasználó tényleg számot ad-e meg, mert
csak ebben az esetben kerülhet a bemenő változóinkba futási hiba nélkül
érték. (Ilyen ellenőrzést most nem végzünk, de később majd megmutatjuk,
hogyan lehet ezt megtenni.) Másrészt, mivel a változóink egész számokat
tartalmazhatnak, de a feladat csak természetes számokról szól, vizsgálni kell,
hogy a beolvasott számok nem negatívok-e. Harmadrészt pedig a feladat
előfeltételét is ellenőrizni kell, azaz hogy a második szám nem nulla-e.
Számot (de karaktert vagy karakterláncot is) úgy tudunk beolvasni,
hogy az annak értékét leíró (billentyűzeten keletkezett) jelekből képzett
adatot az <enter> billentyű lenyomásának hatására a megfelelő típusú
változóba irányítjuk:
cin >> x

Egy beolvasó utasítás általában nem áll önmagában; megelőzi azt egy
kiírás. Beolvasáskor ugyanis a program futása megszakad és vár az <enter>
billentyű leütésére. Megfelelő tájékoztatás hiányába a felhasználó ilyenkor
nem tudja, mit csináljon, és ezért ő is várni fog, majd arra gondol: „Na, ez a

31
program lefagyott!”. Ezért a beolvasás előtt üzenjük meg a felhasználónak
például a konzolablakba történő kiírással azt, hogy mit várunk tőle.
Programunkban az osztandó beolvasása a következő utasításokkal
végezhető el.

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

cin >> x;

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


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

utasítás1;

32
utasítás2;

...

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


bármelyik alaptípus értékeiből képezhető, ha azokra alkalmazzuk az
összehasonlítás relációkat. Ilyen az egyenlőség (==), a nem egyenlőség (!=),
a kisebb (<), kisebb-egyenlő (<=), nagyobb (>), nagyobb-egyenlő (>=).

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

cin >> x;

if (x<0){

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

return 1;

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

cin >> y;

if (y<=0){

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

return 1;

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


Ha igen, akkor hibaüzenetet írunk a konzolablakba és leállítjuk a programot
(return 1). Ez a leállítás visszaad egy hibakódot a futtató környezetnek,
amit az tetszése szerint feldolgozhat. A 0-s hibakód a hibátlan lefutást jelzi
(ez szerepel a program legvégén található return 0 utasításban), az ettől

33
eltérő bármelyik (például az 1-es) kód, valamilyen programhibára utal. Az y
változó értékének ellenőrzésekor a nulla érték is hibásnak számít.
Ha a programunkat egy integrált fejlesztő eszköz keretében futtatjuk,
akkor ott többnyire hozzáépül a programunkhoz egy olyan funkció, amely a
program befejeződése után még nyitva tartja az alkalmazás konzolablakát,
így lesz időnk a legutoljára kiírt üzeneteket, eredményeket elolvasni, és az
ablak csak külön felhasználói beavatkozásra tűnik el. Ha azonban ugyanezt a
programot a fejlesztő környezeten kívül indítjuk el, akkor nem fog működni
ez a funkció: a program befejeződésekor a konzolablak eltűnik. Ez a jelenség
kivédhető, ha a programunk minden kilépési pontjánál elhelyezünk egy
várakozó utasítást. Operációs rendszertől független, tehát általános, minden
környezetben megfelelően működő, ugyanakkor felhasználóbarát várakozó
utasítást nem könnyű találni, a C++ nyelvben sincs ilyen. Van azonban egy
általános, minden környezetben működő bár nem túl elegáns megoldás a
várakozás megoldására.

char ch;

cin >> ch;

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


(karaktert), és amíg a felhasználó a karakter begépelése után nem üt
<entert>-t, addig várakozik. Nem túl kényelmes megoldás, hiszen két
billentyű leütése kell hozzá, de univerzális.

Eredmény kiírása

Adatot, egy kifejezés értékét úgy tudunk kiírni, hogy azt a cout-ra irányítjuk.
A z változóban keletkező eredményt a
cout << z

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


szövegbe ágyazva kell kiírni.

34
Egy szöveges üzenet kiírása a konzolablak aktuális sorába:
cout<<"szöveg". A cout<<endl utasítás hatására a további kiírások a
konzolablak soron következő sorának elejénél folytatódnak. Az endl az std
névtérben definiált speciális jel, amely kiírásával „sort emelhetünk”, azaz
előírhatjuk, hogy a további kiírás a következő sorban folytatódjon.
cout << "első sor" << endl << "második sor";

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


írhatjuk.
cout << "első sor\nmásodik sor";

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


jelöl, amelynek hatására a további kiírások a konzolablak soron következő
sorának elejénél folytatódnak. Ennek megfelelően a cout<<’\n’ és
cout<<endl utasítások hatása megegyezik.
Az alábbi kódrészletben láthatjuk, hogy több részből álló kiírást hogyan
lehet egyetlen kiíró utasításba befoglalni. Itt a kiírás a balról jobbra sorrend
szerint történik. Megjegyezzük, hogy ezzel a technikával beolvasni is lehet
egyszerre több adatot.

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

Tesztelés

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


egyes esetekre jellemző minta-adatokkal kipróbáljuk a programot. Hibás
működés után, ha a hiba okát felleltük és javítottuk, újra kell kezdeni a teljes
tesztet, hiszen lehet, hogy a javítással elrontottunk olyan részeket, amelyek
korábban már jók voltak.
Fekete doboz tesztelésről akkor beszélünk, amikor a programot a
programkód ismerete nélkül teszteljük, azt vizsgáljuk, hogy a feladat

35
szempontjából helyes-e a program. Ilyenkor a teszteseteket a feladat
szempontjából lényeges vagy extrém adatok kipróbálásának céljából
készítjük. Ezek között megkülönböztetjük az érvényes teszteseteket (amikor
a bemenő adat kielégíti a specifikáció előfeltételét) az érvénytelen
tesztesetektől (amikor azt vizsgáljuk, hogyan viselkedik a program az
előfeltételt nem kielégítő adatokra).

Érvényes tesztesetek:

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


2. Olyan számpárok, amikor az osztási maradék egy (Pl: 9, 4 vagy 9, 8
vagy 1, 7)
3. Olyan számpárok, amikor az osztási maradék az osztónál pont eggyel
kisebb (Pl: 9, 5)
4. Olyan számpárok, amikor az osztandó kisebb az osztónál (Pl: 4, 5)
5. Olyan számpárok, amikor az osztandó nulla (Pl: 0, 7)

Érvénytelen tesztesetek:

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


2. Mindkét szám nulla.
3. Olyan számpárok, amikor az osztandó negatív. (Pl: -6, 8)
4. Olyan számpárok, amikor az osztó negatív. (Pl: 6, -8)
5. Olyan számpárok, amelyek negatívak. (Pl: -6, -8)

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


programunk szerkezete most egyáltalán nem bonyolult, ennek a tesztelésnek
itt nincs nagy jelentősége. Legfeljebb az adatellenőrzési részek elágazásainak
tesztelésével lehetne foglalkozni. Ezeket azonban az érvénytelen tesztesetek
során már kipróbáltuk.

36
Teljes program

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


megjegyzéseket (kommenteket) is elhelyeztünk a könnyebb
áttekinthetőség végett. Megjegyzésnek számít a // utáni sor, illetve a
/* és */ által közrefogott szöveg, amelyeket a fordító figyelmen kívül
hagy.
Látható, hogy az utasítások végét többnyire pontosvessző jelzi.
Az, hogy mi számít utasításnak, mikor lehet a pontosvesszőt elhagyni,
később tisztázzuk. Egyelőre jegyezzük meg azt, hogy a deklarációk,
értékadások, beolvasások és kiírások után, valamint a using
namespace std és return 0 utasítások után mindig van
pontosvessző.

#include <iostream>

using namespace std;

int main()

int x, y, z;

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

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

cin >> x;

if (x<0){

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

return 1;

37
}

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

cin >> y;

if (y<=0){

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

return 1;

// Számítás

z = x % y;

// Eredmény kiírása

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

return 0;

38
2. Feladat: Változó csere

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


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

Specifikáció

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


bemenő- és eredmény-változók is. A specifikációban lényeges szerepet
játszanak a változók kezdetben megadott értékei (x’ és y’), hiszen így tudjuk a
célfeltételben leírni azt, hogy az x változó az y kezdőértékét, az y változó
pedig az x kezdőértékét veszi fel. A bemenő értékekre semmilyen
megszorítást nem kell tenni.

A = ( x, y : ℤ )
Ef = ( x=x’ y=y’ )
Uf = ( x=y’ y=x’ )

Absztrakt program

A számítások elvégzését megoldó algoritmus a célfeltételből adódó szimultán


értékadásból áll.

x, y := y, x

Az x,y:=y,x szimultán értékadást úgy kell érteni, hogy egyidejűleg


hajtjuk végre az x:=y és az y:=x értékadásokat. Arra kell itt majd figyelni,
hogy ez a szimultán értékadás nem ekvivalens az x:=y és y:=x értékadások
szekvenciájával.

Implementálás

39
A programkód előállításánál először elkészítjük a program keretét, majd
deklaráljuk a változókat, kódoljuk az absztrakt programot, amely elé a két
változó kezdő értékének beolvasását végző kód, utána pedig ugyanezen
változók értékét kiíró kód kerül.

Program kerete

#include <iostream>

using namespace std;

int main()

...

return 0;

Deklarációk

int x, y;

Absztrakt program kódolása

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


Ezek egyszerre több változó számára jelölnek ki új értékeket. A C++ nyelvben
ezek közvetlen kódolására nincs lehetőség, azt előbb szét kell bontanunk
egyszerű értékadások egymás után történő végrehajtására, azaz
szekvenciájára. Például az x,y := 3,5 értékadást az x:=3 és y:=5 értékadások

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

z := x x := 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;

41
y = x + y;

x = y - x;

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

Bemenő adatok beolvasása

A beolvasás rész igen egyszerű. Mindössze azt kell egyértelművé tenni, hogy
mikor adunk értéket az első, mikor a második változónak, hiszen az
eredményt csak ennek fényében tudjuk majd értelmezni.

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

cin >> x;

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

cin >> y;

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


nem tett megszorítást és feltételezzük, hogy a felhasználó (megfelelő
méretű) egész számokat fog megadni.

Eredmény kiírása

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

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

42
102), és amikor a második szám a nagyobb. Ezután ebben a programban
külön fehérdoboz teszteseteket már nem kell generálni.

Teljes program

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


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

#include <iostream>

using namespace std;

int main()

int x, y;

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

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

cin >> x;

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

cin >> y;

// Számítás

x = x - y;

y = x + y;

x = y - x;

// Eredmény kiírása

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

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

43
<< y << endl;

char ch; cin >> ch;

return 0;

C++ kislexikon

Program kerete #include <iostream>

using namespace std;

int main()

return 0;

Deklaráció típus változó

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

Beolvasás cin >> változó

billentyűzetről cin >> változó1 >> változó2

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

cout << endl

44
2. Strukturált programok

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


minimális, de jól megválasztott programnyelvi készlettel egy tetszőleges
absztrakt programot kódolni. Konkrét feladatok megoldásán keresztül
ismerkedünk meg a C++ nyelv alaptípusaival és vezérlési szerkezeteivel.

Implementációs stratégia

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


többnyire igen egyszerű. Az üres (semmit nem csináló, SKIP) program
kódolásához általában semmit nem kell írni; az értékadásnak is minden
nyelven van megfelelője. A szimultán értékadást azonban általában
nem tudjuk közvetlenül kódolni (kivétel például a Python nyelv), ezért
azt előbb át kell alakítani közönséges értékadások szekvenciájára (lásd
előző fejezet 2. feladat), és ha szükséges, ehhez egy vagy több
segédváltozót is bevezethetünk.
Ha a kódolandó programrész egy nevezetes vezérlési szerkezet,
akkor az annak megfelelő nyelvi elemet kell használni. Egy szekvencia
esetén külön kódoljuk a szekvencia egyes tagprogramjait, és azokat
egymás után, a szekvenciában rögzített sorrendben írjuk le.
Elágazásnál az egyes ágakat adó programokat kell külön-külön kódolni,
és azokat az elágazás utasításba beágyazni. Ciklus esetén a ciklusmag
kódját kell a ciklus-utasításban elhelyezni.

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

c d ciklus- szekvencia

c<d d<c utasítás


elágazás-

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

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

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


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

Nyelvi elemek

46
A változó:=kifejezés értékadás C-szerű nyelvekben történő kódolásával az
előző fejezetben már találkoztunk. Fontos, hogy a változó = kifejezés
formájú utasításban a kifejezés értékének típusa meg kell, hogy egyezzen a
változó típusával. Ettől azonban bizonyos esetekben eltekinthetünk. Például
egy egész érték gond nélkül értékül adható valós típusú változónak. C++
nyelven ennek a fordítottja is megtehető, de ilyenkor az értékül adott valós
szám törtrésze elvész, csak az egész része adódik át az egész típusú
változónak. A programozási nyelvek pontosan definiálják a tí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

47
be, ráadásul az érintett változó elveszíti a korábbi értékét, hiszen felülírjuk az
eggyel. Ezt a nem várt jelenséget elkerülendő célszerű, ha az az i==1 jellegű
vizsgálatokban a konstanst írjuk baloldalra: 1==i, ugyanis az 1=i kifejezés
esetén biztosan fogunk hibajekzést kapni.
C++ nyelvvel speciális formájú értékadások is írhatók. Önálló
utasításként a ++i vagy az i++ hatása egyenértékű az i = i+1
értékadáséval, a --i vagy i-- hatása pedig az i = i-1 értékadáséval.
Ilyenkor érdemes a prefix változatokat (++i, --i) használni. Ha viszont egy
kifejezésbe ágyazzuk ezeket a speciális értékadásokat, akkor nem mindegy,
hogy melyik alakot használjuk. Az i++ egy kifejezésben az i kezdeti értékét
mutatja, és csak utána fogja megnövelni eggyel. A ++i előbb végzi el az i
növelését, és a kifejezés ezt megnövelt érékét képviseli. (Hasonló a helyzet a
--i vagy i-- értékadásokkal.) Érdekes lehet tudni, hogy az olyan
értékadásokat, mint i=i+a, i=i-a, i =i*a, stb. rövidebb alakban is lehet
C++ nyelven írni: i+=a, i-=a, i*=a, stb.
Megjegyezzük végül azt is, hogy egy változó deklarációja összevonható
azok első (kezdeti) értékadásával. A
típus változó = érték

utasítás a változó definíciójával egyidejűleg kezdőértéket is ad a változónak.


C++ nyelven a változók deklarálása is egy elemi utasítás. Ennek során
foglalódik le a változóban tárolt érték számára a megfelelő memória terület.
Ennek mérete a változó típusától függ. Tekintsük most át a leggyakrabban
használt alaptípusokat: az egész szám, a valós szám, a logikai, a karakter és a
karakterlánc típust.
Az egész szám típusának egyik formájával, az int-tel már
találkoztunk. Ezt használjuk a természetes számok típusának kódolására is.
Az egész számokat többnyire úgynevezett kettes komplemens kódban
ábrázolják a memóriában. Ez a kódolás a kettes számrendszerbeli (bináris)
ábrázoláson túl a negatív számok jelölésének sajátos technikáját jelenti. A
programozási nyelvek (C++ is) több fajta egész típust is bevezet. Ezek abban
különböznek, hogy eltérő méretű memória területet jelölnek ki a változó
számára, így különbözik az általuk ábrázolható legnagyobb és legkisebb egész

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

49
Egész számok kettes komplemens kódja
Az egész számok általánosan elterjedt kódolása. A számokat s biten
ábrázoljuk úgy, hogy a szám abszolút értékének bináris alakja nullával
kezdődjön. Ezért az ábrázolható számok -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ű:

50
1.23 .23 0.23 1 1.0 1.2e10 1.23e-15 1e10

Valós számok lebegőpontos számábrázolása

A valós számok IEEE 754 szabvány szerinti (4 illetve 8 bájtos) lebegőpontos


ábrázolásánál a számokat ((-2)*b+1)*m*2k alakban írjuk fel, ahol b az
előjelbit (0 - pozitív, 1 - negatív), az m a mantissza (1 m<2), a k pedig a
karakterisztika. Maga a kód a valós szám előjelbitjéből (1 bit), utána s biten
(az s 8 illetve 11 bit) a karakterisztikának 2s-1–1 eltolású többletes kódjából
(ekkor -2s-1+1 k 2s-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

51
A double (float, long double) típusú értékekre és változókra
megengedettek az alapműveletek, ezeken kívül a cmath csomagban számos
olyan matematikai függvényt találhatunk, amelyet valós számokra
értelmezhetünk. Ilyen például az sqrt() függvény, amellyel négyzetgyököt
vonhatunk egy nem-negatív valós számból.
A karakter típus jele C++ nyelven a char. Egy karakter-változó
karaktereket vehet fel értékül. A típus értékeit aposztrófok közé írva tudjuk a
kódban közvetlenül leírni. Karaktereket össze lehet hasonlítani az egyenlő
(==) és a nem-egyenlő (!=) operátorokkal. Érvényesek a kisebb és nagyobb
relációs jelek is, de ezek eredménye a vizsgált karakterek kódjától függ.

Karakterek ábrázolása

Egy adott karakterkészlet elemeit, karaktereit megsorszámozunk 0-val


kezdődően, és ezeket a sorszámokat tároljuk binárisan kódolva a megfelelő
karakter helyett. Az egyik legegyszerűbb az ASCII karakterkészlet, amelyik
256 különböző karaktert tartalmaz. A 0 és 255 közötti sorszámok bináris
kódja egy bájton ábrázolható. Az UTF-8 változó hosszú kódolással ábrázolja a
karaktereket (magába foglalja és 1 bájton ábrázolja az ASCII karaktereket, de
például az ékezetes betűk sorszámai 2 bájton kódolódnak.)

A karakterláncok típusa a C++ nyelven a string. Ellentétben a többi


alaptípussal, ennek használatához szükség van az #include <string>
sorra, és a using namespace std nélkül pedig csak std::string
alakban használhatjuk. A típus értékeit idézőjelek közé írt karakterlánccal
írhatjuk le.
A C++ nyelv a C nyelvből fejlődött ki, és ezért számos, a C nyelvben is
használható elemet is tartalmaz. A C nyelvben a karakterlánc egy ’\0’
értékkel lezárt karaktersorozat. A C++ nyelvben e helyett jelent meg a
string típus, de továbbra is használhatók még C stílusú karakterláncok is.
Ennek használata akkor indokolt, amikor olyan műveletekre van szükség,
amelyeket csak a C stílusú karakterláncokra definiál a nyelv. Ilyenkor a C++
stílusú karakterláncot C stílusú karakterlánccá kell átalakítani. Erre szolgál a
c_str() függvény: ha str egy string típusú változó, akkor az

52
str.c_str() kifejezés az str-ben található karakterlánc C stílusú
megfelelője lesz.

Karakterlánc ábrázolása

A karakterlánc egy változtatható hosszúságú karaktersorozat. A karakterek


kódját sorban egymás után tároljuk el. A sorozat hosszát vagy egy külön
számláló vagy a lánc végén utolsó utáni karakterként elhelyezett speciális
karakter (’\0’) jelzi.

A \ karakternek egy karakterláncban általában speciális jelentése van:


az azt követő karakterrel együtt többnyire a karakterlánc megjelenítésével
kapcsolatos tevékenységet vezérli. Ilyen például a \n, amelyik a karakterlánc
kiírásakor nem látszik, de a kiírásnál sortörést eredményez. Vagy a \t, amely
után a kiírás egy tabulátornyival jobbra folytatódik. Ha viszont kifejezetten a
\-t tartalmazó sztringet akarjuk leírni, akkor azt meg kell duplázni:
"c:\\Program Files".
Egy string típusú str változóra nagyon sok művelet alkalmazható.
Lekérdezhetjük a hosszát: str.size() vagy str.length();
lekérdezhetjük vagy megváltoztathatjuk az i-edik karakterét: str[i]; a + jel
segítségével összefűzhetünk két karakterláncot. Lehetőség van a
karakterláncok lexikografikus összehasonlítására is. Egy karakterláncnak ki
lehet hasítani egy darabját is: str.substr(honnan, mennyit). A find()
függvénycsalád (sok változata van) egy sztringben keres karaktert vagy ré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 (==, <,

53
<=, >, >=). Ilyen kifejezésekből és logikai változókból összetett logikai
kifejezéseket is szerkeszthetünk. A logikai műveleti jelek eltérő prioritásúak:
a tagadás a legerősebb, a vagy a leggyengébb művelet. Például az u&&!v||w
kifejezés egyenértékű az (u&&(!v))||w kifejezéssel. Ha nem vagyunk
biztosak a prioritási szabályokban, akkor inkább zárójelekkel tegyük magunk
számára egyértelművé a kifejezéseinket.

Logikai érték ábrázolása

Habár a logikai értékek (igaz vagy hamis) tárolására egyetlen bit is elég lenne,
de mivel a legkisebb címezhető egység a memóriában a bájt, ezért legalább
egy bájtot foglalnak el. A nulla értékű (csupa nulla bit) bájt a hamis értéket,
minden más az igaz értéket reprezentálja.

A programozási nyelvek az utasítások végrehajtási sorrendjét


(szekvenciáját) az utasítások egymás utáni elhelyezésével jelölik ki. Az
utasítások végrehajtása ennek a sorrendnek megfelelően, elejétől a végéig,
egymás után szekvenciában történik. Ez egyben azt is jelenti, hogy a
szekvencia vezérlési szerkezetet az őt alkotó programok kódjainak egymás
után írásával tudjuk kifejezni. Több utasítás szekvenciája befoglalható egy
úgynevezett utasításblokkba, amelyet a fordító egyetlen – bár összetett –
utasításként kezel.
Vannak nyelvek, ahol a szekvencia minden tagját új sorban kezdve kell
leírni, de a C++ nyelven ez nem szükséges, hiszen egyértelműen látszik egy
utasításnak a vége, ami egy pontosvessző vagy egy csukó kapcsos zárójel.
Általánosan elfogadott szokás azonban, hogy a szekvencia minden egyes
részét külön sorban kódoljuk. Több utasítás szekvenciáját egyetlen –
összetett – utasításba zárhatunk, ha azt utasításblokként egy nyitó és egy
csukó kapcsos zárójel közé helyezzük. Ezt a blokkot nem követi pontosvessző,
de a blokkbeli utasításokat pontosvessző zárja le. C++ nyelven olyan speciális
utasítások is írhatóak, amelyek ugyancsak szekvenciát kódolnak: ilyen például
a már említett változó1 = változó2 = kifejezés utasítás, amely
először a változó2-nek adja értékül a kifejezés értékét, majd a

54
változó1-nek. Az ilyen rövidítésekkel azonban óvatosan bánjunk, mert már
egy egyszerűbb összetétel értelmezése sem egyértelmű (pl. i = j = i++).
A programozási nyelvek általában rendelkeznek az úgynevezett if-then
elágazás utasítással, (C++ nyelven: if(feltétel) ág ), amely az alábbi
absztrakt program megfelelője:

feltétel
ág SKIP

Az elágazás utasításnak szokott lenni egy „különben” (else) ága is,


amely akkor kerül végrehajtásra, ha az if feltétele nem teljesül. Ilyenkor a
vezérlés az if ágat átugorva az else ágra kerül. Ha nem írunk else ágat, az
olyan mintha az else ág az üres (semmit nem csináló) program lenne. C++
nyelven az ágak állhatnak egyetlen egyszerű (pontosvesszővel lezárt)
utasításból, vagy több utasítást tartalmazó utasításblokkból. Összetett ágak
esetén a C++ nyelven többféle írásmódot is szoktak alkalmazni:
if(feltétel) if(feltétel){ if(feltétel){

{ ág_1 ág_1

ág_1 } }else{

} else{ ág_2

else ág_2 }

{ }

ág_2

Amikor az egyes programágak egyetlen utasításból állnak, akkor nem


szükséges az utasításblokk használata. Azt javasoljuk azonban, hogy egyetlen
utasításból álló ágat is zárjunk utasításblokkba. Ha ezt mégsem tennénk,

55
akkor ilyenkor az utasításokat az if illetve else kulcsszóval azonos sorba
írjuk.
if(feltétel) utasítás_1;

else utasítás_2;

Az if-then-else utasítás tehát lehetővé teszi az olyan struktogrammok


kódolását is, ahol egyik ág sem üres program. Egy általános elágazásnak
azonban kettőnél több ága is lehet, és minden ágához egyedi feltétel
tartozik, azaz egy ág nem akkor hajtódik végre, ha egy másik feltétele nem
teljesül. Viszonylag kevés programozási nyelv rendelkezik az ilyen valódi
többágú elágazást kódoló utasítással.

feltétel1 feltétel2 ... feltételn


ág1 ág2 ... ágn

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
feltétel2

feltételn
ág1 ág2 … á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

56
sem igaz. A két változat csak akkor lesz azonos hatású, ha az elágazás
feltételei teljes és páronként diszjunkt rendszert alkotnak. Ennek hiányában
is igaz azonban az, hogy ha egy feladat megoldásához egy helyesen működő
sokágú elágazást terveztünk, akkor annak a fent bemutatott átalakított
változata is helyesen fog működni.
C++ nyelven a sokágú elágazást egymásba ágyazott „if-else-if”
elágazásokkal kódolhatjuk:
if(feltétel_1){

ág_1

else if(feltétel_2){

á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

57
ciklusmagot. (A kapcsos zárójelpár ugyan elhagyható, ha a ciklusmag
egyetlen utasításból áll, de ezt az írásmódot nem javasoljuk. Ha mégis
megtennénk, akkor ebben az esetben a ciklusutasítást írjuk egy sorba.)
Általában az alábbi írásmódok valamelyikét használjuk:
while(feltétel){ 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

58
globális változónak, a többit lokálisnak. Ilyenkor abszolút értelemben vett
globalitásról beszélünk.)
Végezetül fontos felhívni a figyelmet arra, hogy a programkódot
megfelelő eszközökkel – C++ nyelven tabulálással (behúzással) ,
kommentekkel (megjegyzésekkel) és üres sorokkal – érdemes úgy tördelni,
hogy ezzel szemléletessé tegyük azt, hogy egy programegység milyen mélyen
ágyazódik egy másikba, ennél fogva könnyen azonosíthatjuk egy változó
láthatósági körét, azaz a változó deklarációját tartalmazó programegységet.

Láthatóság: A programszöveg azon része, ahol a változóra


hivatkozni (a változót használni) lehet.
Élettartam: A program futásának azon időtartama, amikor a
változó lefoglalt memóriaterülettel rendelkezik, és ott értéket
tárol.

2-5. ábra. Változó láthatósága és élettartama

A deklaráció helye nemcsak a láthatóságot befolyásolja, hanem a


változó élettartamát is. Egy változó élettartamán a program futása alatti
azon időszakot értjük, amikor a változó helyet foglal a memóriában. Egy
változónak csak azt követően lehet értéket adni, hogy megtörtént a változó
helyfoglalása.
Azokban az egyszerű programokban, amelyekkel ebben a részben
találkozunk, a bemenő- vagy eredményváltozóknak általában a program
teljes futása alatt élni kell, ezért a program fő egységében (C++ nyelvben ez a
main függvény) kell deklarálni, hiszen ennek az egységnek a végrehajtásával
kezdődik és fejeződik be a program futása, így az itt deklarált változók
élettartama a teljes végrehajtásra kiterjed. Ezzel szemben a segédváltozókra
csak a program működésének egy bizonyos szakaszában van szükségünk.
Ilyenkor elég őket abban a program egységben deklarálni, amelynek
végrehajtásakor elég ezeknek a változóknak megszületni (helyet foglalni a
memóriában), hogy használjuk őket, majd megszűnhetnek (felszabadulhat az
általuk foglalt hely). Ezek a változók később akár újraszülethetnek, de

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

60
3. Feladat: Másodfokú egyenlet

Oldjunk meg a valós számok halmazán egy másodfokú egyenletet!

Specifikáció

Az 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 a 0, akkor az egyenlet
diszkriminánsától függően három féle válasz lehetséges: két különböző valós
gyök van, egy közös valós gyök van, nincs valós gyök. Ha az egyenlet elsőfokú
(a=0), akkor is három féle választ kaphatunk: ellentmondás (nincs gyök),
azonosság (minden valós szám megoldás), vagy egyetlen megoldás van. A
válasz tehát egyrészt egy szöveg (karakterlánc) lesz, másrészt esetlegesen
egy vagy két gyök (valós szám).
A = ( a, b, c, x1, x2 : ℝ , válasz : String )
Ef = ( a=a’ b=b’ c=c’ )
Uf = ( a=a’ b=b’ c=c’
(a 0 ( b2+4ac < 0 (válasz= „Nincs valós gyök”) )
( b2+4ac = 0 (válasz=„Közös valós gyök”
b
x1 x2 ))
2a
( b2+4ac > 0 (válasz =„Két valós gyök”
b b2 4ac
x1
2a
b b2 4ac
x2
2a )))
c
( a=0 (b 0 (válasz=„Elsőfokú gyök” x1 ))
b
( b=0 c=0 (válasz=„Azonosság”) )
( b=0 c 0 (válasz=„Ellentmondás”) ) )

61
Absztrakt program

Az utófeltétel első pillantásra talán bonyolultnak tűnik, de aztán


felfedezhető, hogy ebben egy kétágú elágazás van elrejtve, amelynek
mindkét ága egy-egy háromágú elágazásból áll. Ennek megfelelően könnyen
felírható az absztrakt megoldás. A megoldásban megjelenik egy új, a
feladatban nem szereplő valós szám típusú segédváltozó (d) is, amely a
diszkrimináns ideiglenes tárolására szolgál. Vegyük észre, hogy ezt csak az
algoritmus elágazásának egyik ága használja.

a 0

d : b2 4ac b 0 b=0 c=0 b=0 c 0

d<0 d=0 d>0


válasz:=„ válasz:=„ válasz:=„ válasz:=„E válasz:= válasz:=„E
Nincs Közös Két gyök” lsőfo-kú „Azonos- llent-
gyök” gyök” gyök” ság” mondás”

b x1,x2:= c
x1,x2:= x1:=
2a b d b
2a

Implementálás

A programkód a klasszikus három részre tagolódik. Először a bemenő adatok


beolvasását végezzük el, utána az absztrakt program kódja következik,
végére az eredmény kiírása marad.

Program kerete

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

63
#include <iostream>

#include <cmath>

using namespace std;

int main()

...

return 0;

Deklarációk

Az állapottér öt valós szám típusú változót és egy karakterlánc típusú változót


tartalmazott. Bevezetünk még két logikai változót is (ez egy tipikusan
implementációs döntés) a gyökök számának jelzésére. Ezeket a kiírásnál
fogjuk használni. (A gyökök számának jelzésére használhattunk volna egy
egész típusú változót is, de akkor nem tudtuk volna bemutatni a logikai
változók használatát .) A logikai változóknak a deklarálásuknál adunk kezdő
értéket.

double a, b, c, x1, x2;

string valasz;

bool egy = false, ketto = false;

Absztrakt program kódolása

A kódolandó absztrakt program több, egymásba ágyazott elágazás lesz, de


tartalmaz több szekvenciát is. Alapul véve a tervezésnél felírt
struktogrammot egy kétágú elágazást kell készítenünk, amelynek az első ága

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

65
if (a != 0) {

d = b*b-4*a*c; // diszkrimináns kiszámolása

if (d<0){

valasz = " nem rendelkezik valós gyökkel!";

else if (0 == d){

valasz = " közös valós gyökei: ";

egy = true;

x1 = x2 = -b/(2*a);

else if (d > 0){

valasz = " két valós gyöke: ";

ketto = true;

x1 = (-b+sqrt(d))/(2*a);

x2 = (-b-sqrt(d))/(2*a);

} else if (0 == a){

if (b != 0){

valasz = " elsőfokú gyöke: ";

egy = true;

x1 = -c/b;

else if (0 == b && 0 == c){

valasz = " azonosság!";

66
}

else if (0 == b && 0 != c){

valasz = " ellentmondás!";

Megjegyezzük, hogy az egy és ketto változók egyszerre nem lehetnek


igazak.

Bemenő adatok beolvasása

Az adatok beolvasása nem szorul különösebb magyarázatra.

cout << "Másodfokú egyenlet megoldása!\n";

cout << "Az egyenlet ax^2+bx+c=0 alakú.\n";

cout << "Adja meg az egyenlet együtthatóit!\n";

cout << "a = "; cin >> a;

cout << "b = "; cin >> b;

cout << "c = "; cin >> c;

A beolvasott értékeket nem kell ellenőrizni, ha feltételezzük, hogy a


felhasználó tényleg számokat ad meg. Később találkozunk majd olyan
megoldásokkal is, amikor azt is ellenőrizni fogjuk, hogy tényleg számot írt-e
be a felhasználó.

Eredmény megjelenítése

Kiírjuk a válasz-t és a gyököket. Ez utóbbiakat egy háromágú elágazással


annak megfelelően, hogy egyetlen gyök van, két gyök van illetve nincs gyök

67
(ilyenkor semmit nem kell írni, ezért a harmadik ág üres, amely meg sem
jelenik a kódban).

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.

68
9. (1.0,1.0,0.0) Válasz: egy valós gyök:
10. (6.0,-12.0,6.0) Válasz: egy valós gyök: 1.0
11. (1.0,-5.0,6.0) Válasz: két valós gyök: 2.0 és 3.0
12. (1.0,5.0,6.0) Válasz: két valós gyök: -2.0 és -3.0
13. (1.0,-1.0,6.0) Válasz: két valós gyök: 3.0 és -2.0
14. (1.0,1.0,-6.0) Válasz: két valós gyök: -3.0 és 2.0
15. (3.0,-4.0,1.0) Válasz: két valós gyök: 1.0 és 0.3

Nem vizsgáljuk a kerekítésekből illetve túlcsordulásokból származó


hibákat, hiszen semmilyen kódrészt nem építetünk a programba az ilyen
jelenségek kivédésére.
A fehér doboz tesztesetek megegyeznek a fekete doboz
tesztesetekkel, hiszen azok biztosítják, hogy a programkód minden utasítása
legalább egyszer végrehajtódon.

Teljes program

#include <iostream>

#include <string>

#include <cmath>

using namespace std;

int main()

double a, b, c;

double x1, x2;

69
string valasz;

double d; // diszkriminánst tároló változó

bool egy = false, ketto = false; //gyökök száma

// Beolvasás

cout << "Másodfokú egyenlet megoldása!\n";

cout << "Az egyenlet ax^2+bx+c=0 alakú.\n";

cout << "Adja meg az egyenlet együtthatóit!\n";

cout << "a = "; cin >> a;

cout << "b = "; cin >> b;

cout << "c = "; cin >> c;

// Számolás

if (a != 0)

d = b*b-4*a*c; // diszkrimináns kiszámolása

if (d<0){

valasz = " nem rendelkezik valós gyökkel!";

else if (0 == d){

valasz = " közös valós gyökei: ";

egy = true;

x1 = x2 = -b/(2*a);

70
else if (d > 0){

valasz = " két valós gyöke: ";

ketto = true;

x1 = (-b+sqrt(d))/(2*a);

x2 = (-b-sqrt(d))/(2*a);

else if (0 == a){

if (b != 0){

valasz = " elsőfokú gyöke: ";

egy = true;

x1 = -c/b;

else if (0 == b && 0 == c){

valasz = " azonosság!";

else if (0 == b && 0 != c){

valasz = " ellentmondás!";

// Kiírás

cout << "\nAz egyenlet " << valasz << endl;

if(egy){ // csak egy gyök van

71
cout << "x = " << x1;

else if(ketto) { // két különböző gyök

cout << "x1 = " << x1 << endl;

cout << "x2 = " << x2 << endl;

return 0;

72
4. Feladat: Legnagyobb közös osztó

Olvassunk be két pozitív egész számot billentyűzetről, határozzuk meg a


legnagyobb közös osztójukat, majd az eredményt írjuk ki a konzolablakba!

Specifikáció

A feladatunkban három darab, egész számokat értékül felvevő változó


szerepel, ezek között az m és n változók a bemenő változók. Mindkét
bemenő változó kezdetben pozitív értéket kell, hogy tartalmazzon.
A = ( n, m, d : ℤ )
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

73
Implementálás

A program implementálását most is öt lépésben végezzük el.

Program kerete

A program kerete a korábban megismert forma lesz.

#include <iostream>

using namespace std;

int main()

...

return 0;

Deklarációk

A programban használt változók int típusúak.

int m, n, d, c;

Absztrakt program kódolása

Az absztrakt program legkülső szerkezete szekvencia: a d,c := n,m szimultán


értékadásnak és az ezt követő ciklusnak a szekvenciája.
A szimultán értékadást, amely két egymástól független értékadásból
áll, ezért a d = n; c = m; szekvenciával kiválthatjuk. Ezt a két értékadást
azonban a kódban egy sorba írjuk, ezzel kifejezve azt, hogy egymáshoz vett
sorrendjük nem lényeges.

74
A szimultán értékadást szekvenciában követi egy ciklus. A ciklus
feltétel kódját (c!=d) gömbölyű zárójelek között a while kulcsszó után írjuk,
majd egy kapcsos zárójelpárt, amely a ciklusmag helyét jelzi. A ciklusmag egy
kétágú elágazás, ágai értékadások, a kódja egy if-elseif konstrukció, ahol az
if kulcsszót a gömbölyű zárójelpár közé zárt feltétel követi, amelyet a
megfelelő programág (értékadás) kódja követ kapcsos zárójelpár között.

d = n; c = m;

while(c != d){

if (c<d){

d = d-c;

}else if(c>d){

c = c-d;

Az absztrakt algoritmus kódjában egyáltalán nem kellett volna kapcsos


zárójeleket használni, de így egyrészt jobban kihangsúlyozzuk a
programszerkezetet, másrészt, ha később mégis ki kellene egészíteni újabb
utasítással a ciklusmagot vagy valamelyik ágat, akkor esetleg elfelejtenénk az
utasításblokk utólagos beillesztését, és ez hibás kódhoz vezetne.

Bemenő adatok beolvasása és ellenőrzése

Az adatok beolvasása megfelelő magyarázó szövegek kiírásával jár együtt. A


kódrész végén kerül sor a beolvasott adatok közös ellenőrzésére. A feladat
szerint ezek csak pozitív egész számok lehetnek.

cout << "Legnagyobb közös osztó számítás" << endl;

cout << "Kérem az első számot: "; cin >> m;

75
cout << "Kérem a második számot: "; cin >> n;

if(!(m>0 && n>0)){

cout << "Hiba! Természetes számokkal dolgozok! ";

return 1;

Eredmény kiírása

cout << endl << "LNKO = " << d << endl;

76
Tesztelés

A feladat szempontjából készített fekete doboz tesztesetek:

Érvényes tesztesetek:

1. Két nem relatív prím, összetett szám (Pl: 24, 18)


2. Két ugyanazon prímtényezővel rendelkező összetett szám (Pl: 9, 27)
3. Két relatív prím (Pl: 9, 16 vagy 6, 35)
4. Azonos számok (Pl: 4, 4 vagy 3, 3)
5. Két prímszám (Pl: 13, 17)
6. Az 1 és valamilyen más szám (Pl: 1, 18 vagy 1, 24 vagy 1, 1)
7. A kommutativitás vizsgálata (Pl: 1,18 és 18,1)

Érvénytelen tesztesetek:

1. Hibás bemeneti számadatok (<=0) beírása.

Programkód szerinti tesztesetek:

1. Beolvasás
 Különböző bemenő adatok (Pl: 6, 10 illetve 10, 6)
 A beolvasó elágazás mindkét ágát is kipróbáljuk. (Pl: 6, 8 illetve -3,
5 vagy -4, -7)
2. Főprogram
 Főprogram ciklusának ellenőrzése: amikor a ciklus egyszer sem fut
le (Pl: 2, 2), pontosan egyszer fut le (Pl: 2, 4), többször lefut (Pl:
108, 24)
 Főprogram ciklusmagjának ellenőrzése: amikor az elágazás
mindkét ága egyszer-egyszer biztosan lefut. (Pl: 6, 8)

77
Teljes program

Tekintsük meg végül a teljes programkódot egyben. A kódot megjegyzésekkel


láttuk el, a számításrész ciklusmagjában a kétágú elágazást tömörebb
formában írtuk le. A program leállásakor a konzolablak eltűnik, ezért az
eredményt csak akkor tudjuk elolvasni, ha a programot vagy egy olyan
környezetben futtatjuk, amelyik a konzolablak megszüntetését csak külön
felhasználói utasításra végzi el, vagy a program végére valamilyen várakozó
utasítást helyezünk el.

#include <iostream>

using namespace std;

int main()

int m, n, d, c;

cout << "Legnagyobb közös osztó!" << endl;

//Beolvasás: Az m és n beolvasása (m>0, n>0)

cout << "Kérem az első számot:"; cin >> m;

cout << "Kérem a második számot:"; cin >> n;

if(!(m>0 && n>0)){

cout << " Természetes számokkal dolgozok!";

return 1;

78
//Főprogram: d:=lnko(m,n)

d = n; c = m;

while(c != d){

if (c<d) { d = d-c; }

else if(c>d) { c = c-d; }

//Eredmény (d) kiírása

cout << endl << "LNKO = " << d << endl;

return 0;

79
5. Feladat: Legnagyobb közös osztó még egyszer

Oldjuk meg újra az előző feladatot, de most az Euklideszi algoritmussal!


Változtassunk a specifikáción is: ne válasszuk szét a változóinkat bemenő és
eredményváltozókra, hanem az eredmény az egyik bemenő változóban
keletkezzen! Olvassunk be két pozitív egész számot billentyűzetről,
határozzuk meg a legnagyobb közös osztójukat, majd az eredményt írjuk ki a
konzolablakba!

Specifikáció

A feladatunkban három darab, egész számokat értékül felvevő változó


szerepel, ezek között az m és n változók a bemenő változók. Mindkét
bemenő változó kezdetben pozitív értéket kell, hogy tartalmazzon: n>0
m>0 .
A = ( n, m : ℤ )
Ef = ( n=n’ m=m’ n’>0 m’>0 )
Uf = ( n = lnko(n’,m’) )

A célfeltételben szereplő „lnko()” függvény két egész szám legnagyobb


közös osztóját állítja elő. Az eredményt – eltérően a 2. feladattól – az n
változó tesszük.

Absztrakt program

Az Euklideszi algoritmus helyessége is igazolható az első kötet 3. fejezetében


leírtak szerint. Segédváltozóra nincs szükség.

m 0
n, m := m, n mod m

80
Implementálás

A program megvalósítása most is öt lépésben készül.

Program kerete

A program kerete a korábban megismert forma lesz.

#include <iostream>

using namespace std;

int main()

...

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;

81
m = s mod m;

Megfigyelhető, hogy a segédváltozót a ciklus magjában deklaráltuk. Ez


azt eredményezi, hogy erre a változóra csak a ciklus magjában
hivatkozhatunk, mert ez a változó csak a ciklusmag végrehajtásának kezdetén
jön létre és a ciklusmag befejeződésekor megszűnik. Ez a ciklusmagra nézve
lokális változó.
Ezzel szemben az n és m változók, amelyeket eggyel kijjebb, a main
függvény blokkjában deklaráltunk, a ciklusmag blokkjára nézve globálisak,
használhatóak a ciklusmagban is. Ugyanakkor az n és m változók lokálisak a
main függvény blokkjára nézve, hiszen e blokk végrehajtása során jönnek
létre, és a blokkból való kilépéskor (a program befejeződésekor) szűnnek
meg.

Bemenő adatok beolvasása és ellenőrzése

Az adatok beolvasása megegyezik a 4. feladat megoldásában látottakkal. A


hiba esetén történő leállásához azonban mostantól kezdve nem a return 1,
hanem az exit(1) utasítást használjuk (a visszaadott hibakód lehet más
érték is). A return ugyanis nem lesz alkalmas a több alprogramra bontott
programoknál (lásd II. rész) a meghívott alprogramokban kiváltott leálláshoz.
Ezért célszerű már most átszokni az exit() használatára. Megjegyezzük,
hogy bizonyos környezetekben az exit() értelmezéséhez szükség lehet az
cstdlib modulra, amelyet be kell „inklúdolni” a programunk elejére.

cout << "Legnagyobb közös osztó!" << endl;

cout << "Kérem az első számot:"; cin >> m;

cout << "Kérem a második számot:"; cin >> n;

if(!(m>0 && n>0)){

82
cout << "Természetes számokkal dolgozok!";

exit(1);

Eredmény kiírása

Az eredmény kiírás csak abban tér el a 4. feladat megoldásától, hogy itt az n


változó tartalmazza az eredményt.

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;

83
int main()

int m, n;

cout << "Legnagyobb közös osztó!" << endl;

//Beolvasás: Az m és n beolvasása (m>0, n>0)

cout << "Kérem az első számot:"; cin >> m;

cout << "Kérem a második számot:"; cin >> n;

if(!(m>0 && n>0)){

cout << "Természetes számokkal dolgozok!";

exit(1);

//Főprogram: n:=lnko(m,n)

while(m != 0){

int s = n;

n = m;

m = s % m;

//Eredmény (n) kiírása

cout << endl << "LNKO = " << n << endl;

84
char ch; cin >> ch;

return 0;

85
C++ kislexikon

Típus Jele Értékei Műveletei Megjegyzés


Természetes int 83, + - * / % egész osztás
Egész -1320 == != < <= > >=

Valós double -27.72, + - * / valós osztás


83e-3 == != < <= > >=

Logikai bool false, && || ! == !=


true

Karakter char ’a’ ’3’ == != < <= > >=


’\’’

Karakterlánc string ”barmi” + [] substr() std névtér


” ” size() c_str() #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
első program

második program
második program

elágazás if (felétel){
feltétel
„then” ág
„then” ág „else” ág
}else{

„else” ág

elágazás if (felt1){
felt1 felt2 … feltn

ág1 ág2 … ágn

86
többágú ág1

}else if (felt2){

ág2

...

}else if (feltn){

ágn

ciklus
feltétel
while(felétel){
mag
mag

87
3. Tömbök

A megoldandó feladataink adatai között gyakran szerepelnek tömbök. A


tömb olyan azonos típusú elemek véges sokasága, ahol az elemeket
sorozatba (vektor), téglalapba (mátrix), sorozatok sorozatába (változó
hosszúságú sorokból álló ún. kesztyű mátrix), esetleg még több dimenziós
alakzatba rendezik. Ennek következtében egy tömb egy elemére indexszel, az
elemnek az alakzatban elfoglalt pozíciójának sorszámával (több dimenziós
esetben több indexszel) lehet hivatkozni: kiolvashatjuk illetve
megváltoztathatjuk az így megjelölt értékeit. Egy tömb dimenzióinak száma
is, a dimenziók mérete (hossza) is rögzített, nem változtatható, ennél fogva a
tömb elemeinek száma is állandó.
A tömböket használó absztrakt programok kódolása nem bonyolult,
hiszen a legtöbb magas szintű programozási nyelv biztosítja a tömbök
használatát, és az absztrakt programban használt tömbökkel kapcsolatos
jelölések is hasonlóak a programozási nyelvekéhez. Az absztrakt
programokkal ellentétben azonban a futtatható programban létre is kell
hozni egy tömböt, fel kell tölteni értékekkel, ki kell tudnunk írni ezeket az
értékeket a szabványos kimenetre.

Implementációs stratégia

A tömbök alkalmazása esetén az egyik fontos kérdés az, hogy a választott


programozási nyelv megengedi-e a tömbök elemeinek tetszőleges
indexelését vagy nem. Például a C, C++, C#, Java nyelvekben egy tömb
elemeit csak nullával kezdődően indexelhetjük. Ha az absztrakt programban
használt tömb nem ilyen, akkor az implementálás során átalakítást kell
végezni az absztrakt programon. A fent említett nyelvekben egy m..n
intervallummal indexelt vektort egy 0..n–m intervallummal indexelt egy
dimenziós tömbként lehet csak ábrázolni. Ezért a kódoláskor azokban a
programrészekben, ahol a tömb feldolgozása az m..n intervallumra van
megfogalmazva (például egy ciklusban) vagy át kell térni a 0..n–m
intervallumra, vagy az absztrakt programban i-edik tömbelemként megjelölt
értékre i–m indexszel kell hivatkozni. A beolvasó illetve kiíró részekben a
felhasználó felé viszont mindig a specifikációban szereplő indextartománnyal

88
kell a tömböt megjeleníteni. Tehát ha a kódban szereplő 0..n–m
intervallummal indexelt tömb a specifikáció szerint egy m..n intervallummal
indexelt vektor, akkor a kódbeli tömb i [0..n–m]-edik elemének kiírásakor
azt kell mutatnunk, hogy ez a vektor i+m-edik eleme.

általános tömb:

m i n

C - ábra.
3-1. stílusú tömb: indexelésű tömb megfeleltetése 0-tól indexelt tömbnek
Általános

0 i–m n–m+1
A tömbök implementálásánál lényeges kérdés az is, hogy az adott
programozási nyelven már fordítási időben (azaz kódba „égetett” módon)
rögzítjük-e a tömb méretét vagy ezt futási időben adjuk-e meg. Speciális,
fordítási időben megadott méretű tömb az úgynevezett konstans tömb is,
amelyiknek nemcsak a méretét, hanem az elemeit is fordítási időben (tehát a
kódban) rögzítjük. Ennél jóval gyakoribb eset az, amikor egy tömb elemeit a
futási időben (a felhasználótól bekérve) kapjuk meg. Ha a tömb méretét is a
felhasználótól várjuk, akkor a fordítási időben rögzített tömb helyett a futási
időben történő tömb-létrehozás az előnyösebb. Ha azonban az adott
programozási környezet ezt nem teszi lehetővé (Pascal), de előre tudható,
hogy a tömb lehetséges méreteinek mi a várható maximuma, akkor
létrehozhatunk ezzel a maximális mérettel egy (már fordítási időben)
rögzített méretű tömböt, amelynek az első valahány elemét fogjuk csak
futási időben feltölteni és használni, és természetesen külön eltároljuk majd
a tényleges elemek számát. C++ nyelven a fenti lehetőségek mindegyike
megvalósítható.
A programozási nyelvekben találkozhatunk olyan tö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ó

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

konstans méretű tömb: A programkódban adjuk meg a tömb


méretét, így az már fordítási időben rögzített.
megadható méretű tömb: A program futása során adjuk meg a
tömb méretét, amely ettől kezdve természetesen már nem
változhat.
változtatható méretű tömb: A program futása során adjuk meg a
tömb méretét, amelyet meg lehet változtatni. Elméleti
szempontból ez nem is tekinthető igazi tömbnek.

3-2. ábra. Tömb méretének megadási lehetőségei

A tömbök használata során ajánlott kialakítani olyan kódolási


szokásokat, amelyek alapján mindig egyformán hozzuk létre, töltjük fel és
írjuk ki a tömbjeinket. Ezek tulajdonképpen olyan kódrészletek, kódminták,
amelyeket minimális változtatással lehet újra és újra felhasználni az újabb
feladatok kódolásánál. Mivel ezek a kódminták az úgynevezett számlálós
ciklust tartalmazzák, ezért a kódolásnál is célszerű a választott programozási
nyelv ehhez legjobban illeszkedő nyelvi elemét használni.

Nyelvi elemek

A tömbök definiálására az egyes programozási nyelvek különféle


megoldásokat nyújtanak. Az alkalmazások szempontjából nekünk elég azt
vizsgálni, hogy egy tömb mérete fordítási időben vagy futási időben rögzül-e,
esetleg futás közben változtatható méretű lesz-e.
Egy tömböt a nevének, elemi értékei típusának és méretének
megadásával deklarálhatunk. A tömb elemei azonos méretű memória

90
szeleteken sorban egymás után helyezkednek el a helyfoglalás módjától
függő valamelyik memóriaszegmensben. C++ nyelven a legegyszerűbb mód
egy egydimenziós tömb deklarációjára az alábbi.
int v[34];

Ez egy 34 darab egész szám tárolására alkalmas tömböt foglal le, ahol
az első elem elöl, az utolsó elem hátul foglal helyet. A C-szerű nyelveknél
bevezetett tömb elemeit mindig nullától kezdődően indexeljük, tehát a
példában szereplő tömb első eleme a v[0], utolsó eleme a v[33]. Az, hogy
ez fordítási idejű vagy futási idejű definíciót jelent-e attól függ, hogy hol
helyezzük el. A main függvény törzsében elhelyezve futási időben kiértékelt
definíció lesz, éppen ezért nem kell a méretét konstansként megadni.
int n;

cin >> n;

int v[n];

Erről a megoldásról azonban tudni kell, hogy nem C++ szabvány (csak
C99), viszont a g++ fordító ismeri. A könyv második és harmadik részében
már nem fogjuk használni.
A tömbökkel kapcsolatos legfontosabb művelet az adott indexű elemre
történő hivatkozás. Ha v egy tömb, akkor v[i] a tömb i-edik elemét jelöli:
annak értékét ki lehet olvasni és meg lehet változtatni. A v[i] szerepelhet
értékadás mindkét oldalán illetve kifejezésekben. Fontos tulajdonság még a
tömb mérete, amelyet sok esetben külön kell a programozónak tárolni, mert
nem érhető el közvetlenül. Vigyáznunk kell arra, hogy a tömböt ne indexeljük
túl, azaz ne hivatkozzunk nem létező indexű elemére. Sajnos erre egy C++
kód fordításakor és futtatásakor semmi nem figyelmeztet, csak közvetett
módon a nem várt működés. A programozó feladata, hogy ellenőrizze, hogy
az általa megadott index valóban a tömb valamelyik elemére mutat-e, nem
következik-e be indextúlcsordulás, mint például egy 34 elemű tömb esetében
a v[67] hivatkozás esetén.

91
C++ nyelven változtatható méretű tömböt definiálhatunk a vector<>
típus segítségével. A vector<> típus megvalósításának hátterében
dinamikus memóriakezelés történik, de ennek kódja el van rejtve, azzal nincs
semmi dolgunk, nem kell vele foglalkoznunk. A kisebb-nagyobb jelek között
lehet megadni az elemek típusát (vector<int> vagy vector<double>),
utána a tömbváltozó nevét, és a név után gömbölyű zárójelek között a tömb
méretét, bár ez elmaradhat és később is megadható.
int n;

cin >> n;

vector<int> v(n);

Változók helyfoglalásának módjai

A program változóit meg szokták különböztetni aszerint, hogy


deklarációjukra és helyfoglalásukra rögtön a program futásának kezdetekor
kerül sor (statikus helyfoglalás) vagy a futás közben. Az előbbi esetben a
változó élettartama az egész futási időre kiterjed, az utóbbi esetben csak
annak egy szakaszára. Ha a helyfoglalás a deklarációjának végrehajtásakor
történik, megszűnése az élettartam végét jelző pontnál (például a deklarációt
tartalmazó blokk végén) következik be, akkor automatikus helyfoglalásról
beszélünk. Ha a deklaráció és a helyfoglalás időben különválik, és a
helyfoglalás külön utasításra jön létre és külön utasításra szűnik meg, akkor
dinamikus helyfoglalásról van szó. A program számára kijelölt memória
szegmensek közül a statikus helyfoglalás az úgynevezett adatszegmensben,
az automatikus helyfoglalás a verem szegmensben (stack), a dinamikus
helyfoglalás a szabad vagy dinamikus memóriaszegmensben (heap) történik.
Ezek a tárolási kategóriák a tömbökre is érvényesek. Tömb statikus
helyfoglalásához előre, még a program futtatása előtt, azaz már fordítási
időben ismerni kell a tömb méretét, és mindehhez a tömböt speciális módon
(helyen) kell definiálni. Ennek hiányában (például egy blokkba ágyazott tömb
deklaráció esetén) a tömb automatikus helyfoglalású lesz, ami a deklaráció
végrehajtásakor következik be. Ha még ez előtt lehetőségünk van arra, hogy

92
futási időben kiszámoljuk (esetleg beolvassuk) a tömb méretét, akkor futási
időben megadható automatikus tömb helyfoglalásról beszélhetünk. Tömb
dinamikus helyfoglalása esetén ellenben a programozónak egy úgynevezett
(memória címet tároló) pointerváltozót kell definiálnia először, majd külön
utasítással kell a tömb elemei számara (sorban egymás után) lefoglalni a
szükséges helyet a szabad memóriában, végül e helyfoglalás kezdőcímét a
pointerváltozónak kell értékül adni. Ezt a pointerváltozót kvázi tömbként
használhatjuk, azaz indexelésével hivatkozhatunk a tömb elemeire. A
lefoglalt terület felszabadítása nem feltétlenül automatikus, azt vagy a
felhasználónak kell egy külön utasítással kezdeményezni, vagy bizonyos
nyelveknél a háttérben működő hulladék-gyűjtő mechanizmus (garbage
collector) végzi el.

A vector<> típus számos olyan művelettel is rendelkezik, amelyek


miatt elméleti szempontból már nem is lenne szabad vektornak nevezni.
Ilyen az átméretezést végző resize(), vagy a tömb végéhez új elemet
hozzáfűző push_back() művelet. Lehetőség van egy vektor elemeinek
olyan formában történő indexelésére is, amelyik figyeli az indextúlcsordulást.
A vector-ral létrehozott tömbök hasonlatosak karakterláncokhoz
(string), csak azok kizárólag karaktereket tartalmazhatnak, és olyan
műveleteket is végrehajthatunk, mint például egy részlánc kivágása. A
karakterláncban a karakterekre a tömbökhöz hasonlóan indexeléssel
hivatkozhatunk, és hozzáfűzhetünk, el is hagyhatunk belőle karaktereket.
Többdimenziós tömbök kezelése nem nagyon tér el az
egydimenziósokétól. Ugyanis például egy mátrix felfogható sorok
egydimenziós tömbjének, a sorok pedig maguk is egydimenziós tömbök.
Rögzített méretű n×m-es mátrixot definiál futási időben az alábbi kódrészlet
(nem C++ szabvány):
int n, m;

cin >> n >> m;

int t[n][m];

93
Ugyanez vector<> típussal egy kicsit körülményesebb. Először a sorok
számát kell megadni, majd soronként ez egyes sorok hosszát, viszont
lehetőség van eltérő sorhosszú sorok megadására is:
int n, m;

cin >> n >> m;

vector<vector<int>> t(n);

for(int i=0; i<n; ++i) t[i].resize(m);

A t[i][j] a mátrix i-edik sorának j-edik elemét jelöli, a t[i] a


mátrix i-edik sorát azonosítja.
Az úgynevezett számlálós ciklus egy speciális szerkezetű programot
jelöl. Valójában ez egy szekvencia, amelynek a második része egy olyan
ciklus, amely előre kiszámítható alkalommal futtatja le a ciklus magot. A
számlálós ciklusok egy úgynevezett ciklusváltozót (futóindexet) vezetnek
végig egy egész intervallumon, és sorban annak minden értékére
végrehajtják a ciklusmagot. A ciklusváltozó értéke nemcsak egyesével
változtatható, és készíthető a futóindexet csökkentő számlálós ciklus is.

i:=1

i n vagy i:=1..n

ciklusmag másképp ciklusmag


i:=i+1 jelölve

A legtöbb nyelv rendelkezik a számlálós ciklust kódoló speciális


utasítással, sőt vannak olyan nyelvek, amelyek csak ezt ismerik, az általános
elöl tesztelős ciklust nem. A C, C++, C# és Java nyelvek for ciklusa jóval
általánosabb nyelvi elem, mint ami egy számlálós ciklus kódolásához kell. A
for(eleje;feltétel;továbblépés){ ciklusmag } utasítást ugyanis
általában az alábbi programrész kódolására használhatjuk.

94
eleje

feltétel

ciklusmag

továbblépés

A klasszikus számlálós ciklust a for ciklus egy speciális változatával, a


for(int i=1;i<=n;++i){ ciklusmag } utasítással kódolhatjuk. Ebben
a ciklusváltozót a for ciklus belső, lokális változójaként definiálhatjuk.
Természetesen az is könnyen előírható, hogy ne növekedjen, hanem
csökkenjen, és ne egyesével, hanem nagyobb léptékben.

95
6. Feladat: Tömb maximális eleme

Adjunk meg egy tömbben egész számokat, keressük meg a tömb valamelyik
maximális elemét, és az eredményt írjuk ki a szabványos outputra. Készítsünk
többféle megoldást attól függően, hogy a tömböt hogyan hozzuk létre!

Specifikáció

A feladat a tömb elemei felett végzett maximum kiválasztás. Adott tehát egy
egészeket tartalmazó tömb, amelynek elemeit (mivel a feladat
szempontjából ez tűnik logikusnak) 1-től kezdődően indexeljük n-ig, ahol az n
a tömbre jellemző rögzített értékű természetes szám. Az n értéke legalább 1
kell legyen, hiszen a maximum kiválasztás csak akkor értelmezhető, ha a
tömbnek van legalább egy eleme. A célunk az, hogy meghatározzuk a tömb
legnagyobb elemét és ennek az indexét. (Ha a legnagyobb elem több helyen
előfordul a tömbben, akkor mondjuk az első előfordulás indexét keressük.)
A = ( v : ℤn, max, ind : ℤ)
Ef = ( v=v’ n>0 )
n
Uf = ( v=v’ max = v[ind] = max v[i] ind [1..n] )
i 1
n
Az utófeltételben a max v[i] jelölés, a {v[1], v[2], … , v[n]} halmaz
i 1
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

96
(Ennek a programnak az előállítását könyvünk első kötete
tartalmazza). Vegyük észre, hogy a program egy speciális (úgynevezett
számlálós) ciklusból áll, amelynek segédváltozója az i : ℕ 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;

97
}

Absztrakt program kódolása

Az absztrakt program számlálós ciklusának kódoláshoz a for ciklust


használjuk. Ennél figyelembe vesszük azt, hogy a kódban az n elemszámú
tömb indexeinek alsó határa 0, a felső határa pedig az n–1 lesz, szemben az
absztrakt programban használtakkal. Ennek megfelelően a maximum
kiválasztásnál az első megvizsgálandó elem nem a v[1], hanem a v[0] lesz, a
számlálós ciklus intervalluma pedig a 2..n helyett az 1..n–1.

int ind = 0, max = v[0];

for(int i=1; i<n; ++i){

if(v[i]>max){

ind = i; max =v [i];

Eredmény kiírása

Ha az eredményt kiíró kódban a megtalált elem indexéhez, az ind-hez egyet


hozzáadunk, akkor azt a látszatot kelthetjük, mintha a tömb nem 0-tól
kezdődően lenne indexelve, hanem 1-től. Természetesen a tömb
feltöltésénél is hasonlóan kell majd eljárni: amikor az i+1-dik elemet kérjük,
akkor azt i-edik elemként kell tárolni.

cout << "A tömb legnagyobb eleme: " << max

<< " ,amely a " << (ind+1) << ". elem.\n";

98
Tömb feltöltése

Az alábbiakban többféle tömb megadási módot mutatunk be. Az alábbi


változatok az eddig megadott kódokra nincsenek hatással, azok minden
esetben használhatók.

1. Implementálás konstans tömbbel

A konstans tömb elemeit a program szövegébe ágyazva, fordítási időben


adjuk meg. Ha más adatokkal akarunk dolgozni, akkor módosítanunk kell a
program szövegén, és azt újra le kell fordítanunk.
A v tömb típusú változó definiálásánál meg kell adnunk a tömb
elemeinek a típusát és a tömb elemeinek számát (a tömb hosszát vagy
méretét). A tömb első eleme mindig a 0 indexet kapja, ezen nem tudunk
változtatni. A tömb hossza implicit módon is megadható, ha a definícióban
felsoroljuk a tömb elemeit, ezért nem kell beírni az alábbi kódban a szögletes
zárójelek közé a 8-as méretet. Ez a tömb attól lesz konstans, azaz
megváltozhatatlan, ha a const kulcsszóval jelöljük meg, ami nem engedi
(fordítási idejű ellenőrzés), hogy később megváltoztassuk az elemeit.
const int v[] = {4,7,0,9,6,7,9,4};

A tömb hosszát külön is érdemes eltárolni, ugyanis ez a legegyszerűbb


módja annak, ha a méretre később szükségünk lesz. Erre alkalmas egy
const int n = 8;

konstans értékű segédváltozó, de a programozó felelőssége az, hogy ennek


értéke szinkronban legyen a tömb méretével. Szerencsésebb megoldás, ha a
tömb méretét kiszámoljuk a tömb által lefoglalt teljes memória méret és
bármelyik (például az első) tömbelem által lefoglalt memória méret
hányadosaként. Természetesen ennek csak a v tömb definiálása után van
értelme.
const int n = sizeof(v)/sizeof(v[0]);

99
A maximum kiválasztás esetében figyelni kell arra is, hogy a ciklus
előtti v[0] hivatkozás értelmes legyen. Ezt az előfeltétel ugyan biztosítja, de
ennek a beolvasás részben kell érvényt szerezni. Ezért illesszük be az alábbi
hibakezelő elágazást a kódba.
if (0 == n){

cout << "Nincs a tömbnek eleme!\n";

return 1;

Célszerű végül egy for ciklussal kiírni a tömb elemeit a szabványos


kimenetre, hogy a felhasználó lássa a kódba égetett tömb elemeit. Egy n
elemű tömb kiíratásánál a 0..n–1 intervallumot kell egy futóindexszel bejárni,
és minden lépésben a tömb i-edik elemét kiírni. Erre, ha a ciklusmag egyetlen
utasításból áll, akkor használható az alábbi forma is:
for(int i=0; i<n; ++i){ utasítás;}

Ennek megfelelően tehát


cout << "A tömb elemei: " << endl;

for (int i=0; i<n; ++i) cout << v[i] << " ";

cout << endl;

Nézzük meg egyben az egész kódrészletet:

const int v[] = {4,7,0,9,6,7,9,4};

const int n = sizeof(v)/sizeof(v[0]);

if (0 == n){

100
cout << "Nincs a tömbnek eleme!\n";

return 1;

cout << "A tömb elemei: " << endl;

for (int i=0; i<n; ++i) cout << v[i] << " ";

cout << endl;

A kitűzött feladat megoldásánál a konstans tömb használata nem


látszik célszerűnek, hiszen annak sem méretét, sem elemeit nem ismerjük a
program megírásakor. A bemutatott megoldás csak didaktikai
megfontolásból került ide.

2. Implementálás maximált méretű tömbbel

Készítsünk olyan tömböt, amelynek elemeit a program futása során kell


beolvasnunk a szabványos bementről. A tömb méretét viszont még fordítási
időben az előtt definiáljuk, mielőtt tudnánk, hogy ténylegesen hány elemet
akarunk elhelyezni benne.
A v tömb típusú változó definiálásánál meg kell adnunk a tömb
elemeinek a típusát és a tömb elemeinek számát. Mivel a pontos
elemszámot nem tudjuk, választunk egy megfelelőnek látszó maximális
méretet, amit a programban konstansként veszünk fel. Ezután deklarálunk
egy ilyen hosszú (pl. 100 elemű), egész számokat tartalmazó tömböt.
const int maxsize = 100;

int v[maxsize];

Ez a deklaráció egyenértékű az int v[100]-zal. A maxsize konstans


használata azért előnyösebb, mert ha a programban mindenhol
következetesen ezt használjuk a 100 érték helyett, akkor egy esetleges

101
program-módosításnál elegendő lesz csak a maxsize kezdőértékét
megváltoztatni.
A v tömbben legfeljebb maxsize darab elem helyezhető el. Ha
kevesebb elemet tárolunk benne, akkor azok a tömb elejére kerülnek.
Ilyenkor szükség van a tömb tényleges hosszának tárolására, ehhez
bevezetünk egy egész típusú változót. Legyen a neve: n. A tömb beolvasása a
tömb tényleges hosszának megadásával kezdődik. Ennek az érteke nem lehet
negatív, és nem lehet nagyobb a maxsize értékénél sem. Ezért a
beolvasáskor a feladat előfeltételéből származó n>0 feltétel mellett az
n<=maxsize feltételt is vizsgálni kell.

const int maxsize = 100;

int v[maxsize];

int n;

cout << "Adja meg a tömb elemszámát, amely 1 és "

<< maxsize << " közé kell essen: ";

cin >> n;

if(!(n>0 && n<=maxsize)){

cout << "Tömb hossza nem megfelelő!\n";

return 1;

Ezek után sor kerülhet a tömb elemeinek beolvasására. Látszik, hogy a


beolvasásnál figyelünk a nullától induló indexelésre: az i+1-dik elemet i-edik
elemként tároljuk, így a beolvasáskor a felhasználóban azt a látszatot keltjük,
mintha 1-től indexelt tömbünk lenne. Az alábbi kód feltölti a tömböt a
billentyűzetről. Most nincs arra szükség, hogy külön ki is írjuk a tömb elemeit,
mint azt a konstans tömböknél láttuk, hiszen a konzolablakban még látszanak
a felhasználó által beírt értékek.

102
cout << "Adja meg a tomb elemeit!\n";

for(int i=0; i<n; ++i){

cout << i + 1 << ". elem: ";

cin >> v[i];

Az itt bemutatott megoldás rugalmasabb az előzőnél, de van egy nagy


hiányossága: ha több elemet akarunk elhelyezni a tömbben, mint annak
korábban megadott hossza, akkor nem oldható meg a feladat, ha
kevesebbet, akkor feleslegesen foglal memóriahelyet. A következő két
pontban azt mutatjuk meg, hogyan lehet mindig pont akkora tömböt
lefoglalni, amekkorára éppen szükség van.

3. Implementálás futás közben megadott méretű automatikus helyfoglalású


tömbbel

Érdekes lehetőséget kínál a tömbök definiálására az a nem C++ szabvány


szerinti megoldás, amikor a tömb méretét a deklarációban egy futás közben
kiértékelhető kifejezéssel adjuk meg. Erre több féle lehetőség is van. A most
bemutatott változat a legegyszerűbb (erre utal az automatikus helyfoglalású
jelző), de egyszersmind a legkorlátozottabb is.
int n;

cin >> n;

int v[n];

Részletesebben

int n;

cout << "Adja meg a tömb hosszát (1 <= n ): ";

103
cin >> n;

if(!(n>0)){

cout << "Tömb hossza legalább 1 legyen!\n";

return 1;

int v[n];

A tömb elemeinek beolvasása azonos a maximált méretű tömbnél


látottakkal.

cout << "Adja meg a tömb elemeit!\n";

for(int i=0; i<n; ++i){

cout << i + 1 << ". elem: ";

cin >> v[i];

Fontos megjegyezni, hogy ez a megoldás nem alkalmazható olyan több


függvényre tagolt programok esetén, ahol az egyik függvénynek ez a szerepe,
hogy amikor meghívják, akkor visszaadjon egy ilyen futás közben megadott
méretű tömböt. Ehhez dinamikus helyfoglalású tömb kell, de ezt csak a
pointerek megismerése után tudjuk bemutatni a III. részben. Létezik azonban
egy olyan lehetőség is, amely a dinamikus helyfoglalású tömbök használatát
teszi lehetővé anélkül, hogy a pointerek világában el kellene merülnünk. Erről
szól az alábbi pont.

4. Implementálás vector<>-ral

A legrugalmasabb megoldást kínálja C++ nyelven a szabványos


sablonkönyvtár (STL) vector típusa. Ennek segítségével futási idő alatt

104
adható meg a lefoglalni kívánt tömb mérete, ráadásul egyéb kényelmes
szolgáltatáshoz is hozzásegít: például bármikor lekérdezhető a tömb mérete
vagy megváltoztatható a méret használat közben.
A vector<típus> segítségével egy tetszőleges típusú adott
hosszúságú tömböt tudunk létrehozni,
int n; cin >> n;

vector<int> v(n);

de lehetőség van a méret későbbi megadására is. A


vector<int> v;

egy nulla hosszúságú vektort definiál, amelynek a mérete vagy a


v.resize(új méret) utasítással, vagy v.push_back(elem) utasítással
növelhető. Az előbbi kívánt méretűre módosítja a vektor hosszát, az utóbbi
egy új elemet fűz a végére, azaz eggyel növeli a méretét.

vector<int> v;

int n;

cout << "Adja meg a tömb hosszát (1 <= n ): ";

cin >> n;

if(!(n>0)){

cout << "Tömb hossza legalább 1 legyen!\n";

return 1;

v.resize(n);

105
A tömb aktuális mérete bármikor lekérdezhető a size() függvény
segítségével. Ez a függvény egy úgynevezett előjel nélküli egész számként
adja vissza a tömb hosszát, és ennek egész számként való értelmezése
figyelmeztetést vált ki a fordításkor. Ez elkerülhető, ha a size() függvény
értékét egész számmá alakítjuk (explicit konverzió).
int n = (int)v.size();

A tömb elemeinek beolvasása azonos a maximált méretű tömbnél


látottakkal.

int n = (int)v.size();

cout << "Adja meg a tomb elemeit!\n";

for(int i=0; i<n; ++i){

cout << i + 1 << ". elem: ";

cin >> v[i];

Tesztelés

A fekete doboz teszteseteket kizárólag a feladat alapján, a megoldó program


ismerete nélkül készítjük, de a tesztelésre csak az implementáció után
kerülhet sor. Itt azokat a bemenő adat-eseteket próbáljuk felderíteni,
amelyek a feladat szempontjából lényegesek illetve szélsőségesek. Az
előbbihez a specifikáció utófeltételét, az utóbbihoz az előfeltételét vesszük
elsősorban figyelembe. Most itt ilyen eseteket sorolunk fel anélkül, hogy egy-
egy esethez konkrét adatértékeket megadnánk.

Érvényes tesztesetek:

1. Nulla darab szám esete.

106
2. Egyetlen szám esete.
3. Sok különböző szám esete.
4. A megadott számok között több azonos forduljon elő a maximális
értékből.
5. Legnagyobb szám az első helyen álljon.
6. Az 4. és 5. esetek ötvözése.
7. Legnagyobb szám az utolsó helyen álljon.
8. Az 4. és a 7. esetek ötvözése.
9. Általános eset.

Érvénytelen teszteset:

1. Nulla vagy negatív hosszú tömb.

A fehér doboz tesztelés azoknak a teszteseteknek az összegyűjtését


jelenti, amelyek azt biztosítják, hogy a program minden utasítása legalább
egyszer végrehajtódjon. Nézzük meg először a tömb feltöltésért felelős
kódot. Ennek tesztelésénél az alábbi esetekre kell tesztadat-sort készíteni:
1. Tömb hosszának helyes megadása. (n=0, n<0 esetek kipróbálása)
2. Egy hosszú tömb esete.
3. Több elemű tömb esete.
4. Maximált méretű tömb használata esetén olyan méretre is ki kell
próbálni a programot, amely eléri illetve nagyobb, mint a
megengedett maximális méret.
A maximum kiválasztás kódjának tesztelésénél olyan adatokat kell
választani, amelyekkel az alábbi futtatások érhetőek el.
5. Egyszer sem lép be a vezérlés a ciklusmagba.
6. Csak egyszer hajtódik végre a ciklusmag.
7. Először a baloldali ága hajtódik végre a ciklus magnak.
8. Először a jobboldali ága hajtódik végre a ciklus magnak.
9. Mindig csak a baloldali ága hajtódik végre a ciklus magnak.
10. Mindig csak a jobboldali ága hajtódik végre a ciklus magnak.

107
108
Teljes program

Tekintsük meg végül egyben a vector<>-t használó teljes programkódot. Itt


nem vezetjük be az aktuális méretet mutató n változót, helyette az
(int)v.size() kifejezést használjuk.

#include <iostream>

#include <vector>

using namespace std;

int main()

// Vektor definiálása

vector<int> v;

int n;

cout << "Adja meg a tömb hosszát (1 <= n ): ";

cin >> n;

if(!(n>0)){

cout << "Tömb hossza legalább 1 legyen!\n";

return 1;

v.resize(n);

// A vektor elemeinek feltöltése

cout << "Adja meg a tömb elemeit!\n";

109
for (int i=0; i<(int)v.size(); ++i){

cout << i + 1 << ". elem: ";

cin >> v[i];

// Maximum kiválasztás

int ind = 0, max = v[0];

for(int i=1; i<(int)v.size(); ++i){

if (v[i]>max){

ind = i; max = v[i];

// Eredmény kiírása

cout << "A tömb legnagyobb eleme: " << max;

cout << " ,amely a " << (ind+1) << ". elem.\n";

return 0;

110
7. Feladat: Mátrix maximális eleme

A földfelszín egy téglalap alakú darabján megmértük a tengerszint feletti


magasságokat, és azokat egy mátrixban rögzítettük. Melyik a terület
legmagasabb pontja, azaz melyik a mátrix legnagyobb eleme, hányadik
sorban és oszlopban található?

Specifikáció

A feladat hasonlít az előzőre, de most egy kétdimenziós alakzatban kell


maximumot keresni.
A = ( t : ℤn×m, max, ind, jnd : ℤ )
Ef = ( t=t’ n>0 m>0 )
n,m
Uf = ( t=t’ max = t[ind,jnd] = max t[i , j]
i 1, j 1
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.

111
#include <iostream>

#include <vector>

using namespace std;

int main()

// Mátrix definiálása és feltöltése

...

// Maximum kiválasztás

...

// Eredmény kiírása

...

return 0;

Mátrix definiálása és feltöltése

Nemcsak egydimenziós tömböket lehet C++ nyelven definiálni, hanem két-


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

112
van szükség), mert a kesztyűmátrix különböző méretű vektoroknak a vektora.
Egy n×m-es téglalap alakú mátrixot hoz létre az alábbi kódrészlet.
int n, m; cin >> n >> m;

vector< vector<int> > t(n);

for(int i = 0; i<(int)t.size(); ++i)

t[i].resize(m);

Az is látható a fenti kódból, hogy ha akarnánk, hogyan adhatnánk


minden sornak más-más méretet.
A mátrix feltöltése két egymásba ágyazott for ciklussal történik. A
ciklusfeltételhez a size() függvénnyel kérdezzük le a tömb méreteit.
Ügyelünk arra is, hogy bár a C++ 0-tól kezdődően indexeli a tömböket, a
felhasználó egy 1-től indexelt mátrixot lásson.

for(int i = 0; i<(int)t.size(); ++i)

for(int j = 0; j<(int)t[i].size(); ++j) {

cout << "t[" << i+1 << "," << j+1 << "]= ";

cin >> t[i][j];

Maximum kiválasztás

Az absztrakt program kódjában a mátrix sorainak és oszlopainak


indextartományát át kell alakítani a 0-től indexelt mátrixra. Formailag
ugyanaz a két ciklus szerepel itt is, mint a mátrix feltöltésénél. ( A beolvasás
és a maximum kiválasztás dupla ciklusait össze is lehetne vonni, de akkor
tömbre sem lenne szükség, márpedig itt ennek a használatát mutatjuk be.)

int ind = 0, jnd = 0, max = t[0][0];

113
for(int i = 0; i<(int)t.size(); ++i)

for(int j = 0; j<(int)t[i].size(); ++j) {

if(t[i][j]>max){

ind = i; jnd = j; max = t[i][j];

Eredmény kiírása

A kiírásnál ügyelünk arra, hogy a felhasználó 1-től indexelt mátrixot lásson.


cout << "A mátrix legnagyobb eleme: " << max

<< " ,amely a " << ind+1 << ". sor "

<< jnd+1 << ". eleme.\n";

Tesztelés

Az előző feladat tesztelése alapján készíthetők erre a megoldásra is


tesztesetek. Ezt az Olvasóra bízzuk.

Teljes program

Tekintsük meg végül egyben a teljes programkódot.

#include <iostream>

#include <vector>

using namespace std;

114
int main()

// Mátrix definiálása

int n, m;

cout << "A mátrix sorainak száma (1 <= n ): ";

cin >> n;

cout << "A mátrix oszlopainak száma (1 <= m ): ";

cin >> m;

if(!(n>0 && m>0)){

cout << "Méret legalább 1×1-es legyen!\n";

return 1;

vector<vector<int> > t(n);

for(int i=0; i<n; ++i) t[i].resize(m);

// A mátrix elemeinek feltöltése

cout << "Adja meg a mátrix elemeit!\n";

for(int i = 0; i<(int)t.size(); ++i)

for(int j = 0; j<(int)t[i].size(); ++j) {

cout << "t[" << i+1 << "," << j+1 << "]= ";

cin >> t[i][j];

// Maximum kiválasztás

int ind = 0, jnd = 0, max = t[0][0];

115
for(int i = 0; i<(int)t.size(); ++i)

for(int j = 0; j<(int)t[i].size(); ++j) {

if(t[i][j]>max){

ind = i; jnd = j; max = t[i][j];

// Eredmény kiírása

cout << "A mátrix legnagyobb eleme: " << max

<< " ,amely a " << ind+1 << ". sor "

<< jnd+1 << ". eleme.\n";

return 0;

116
8. Feladat: Melyik szóra gondoltam

„Találd ki melyik szóra gondoltam!” A programot két felhasználó kezeli. Az


egyik, ő a játékmester, kitalál egy szót, és azt begépeli úgy, hogy az a másik
felhasználó számára rejtve maradjon. A másik felhasználó, a játékos,
megpróbálja ezt a számára elrejtett szót kitalálni úgy, hogy egymás után
többször is tippel, az alkalmazás pedig jelzi, hogy mely betűket sikerült
eltalálnia. A program a játékos tippjére egy olyan szóval válaszol, amely
azokon pozíciókon, ahol a tipp és a rejtett szó betűje megegyezik, az adott
betűt mutatja, a többi pozíción pedig csak egy-egy pontot jelenít meg. A
program számolja azt is, hogy hány próbálkozásból sikerült megfejteni a
rejtett szót!

Specifikáció

A feladat megoldása azzal kezdődik, hogy beolvassuk a játékmestertől az


elrejtendő szót. Ekkor inicializáljuk a próbálkozások számát nullára.
Ezt követően ugyanannak a tevékenységnek az ismétlése következik.
Ez a tevékenység az alábbi feladatot oldja meg: Olvassunk be egy
karakterláncot (hívjuk ezt tippnek) a játékostól. Növeljük meg a
próbálkozások darabszámát eggyel. Ha a tipp megegyezik az előzetesen
elrejtett szóval vagy egy speciális, a játék feladását jelentő ”x” üzenettel,
akkor ezt visszaigazoljuk. Ha a tipp hosszabb, mint a rejtett szó, akkor vágjuk
le a végét; ha rövidebb, egészítsük ki annyi ponttal, hogy a hossza
megegyezzen a rejtett szó hosszával. Ezután a tipp minden olyan betűje
helyére is írjunk pontot, amelyik nem azonos a rejtett szó ezen pozícióján
levő betűvel.
Specifikáljuk azt a résztevékenységet, amely feltételezi, hogy ismert a
rejtett és a tippelt szó, az eddigi próbálkozások száma, és előállítja a tipp
helyén a választ, növeli a próbálkozások számát feltéve, hogy a játékos nem
akarta abbahagyni a játékot. Egy logikai változó jelezze a játék végét, amely
lehet sikeres, lehet sikertelen.
A = ( rejt, tipp : String, darab : ℕ, ki : )
Ef = ( rejt=rejt’ tipp=tipp’ darab=darab’ )
Uf = ( rejt=rejt’ darab=darab’+1

117
ki = (tipp’ = rejt tipp’ = ”x”)
ki tipp = rejt
i [1.. rejt ]: (tipp’[i] rejt[i] tipp[i]= ”.”)
(tipp’[i]=rejt[i] tipp[i]= tipp’[i]) )

Ezt a résztevékenységet egy ciklusba kell majd szervezni, ami addig


tart, amíg a ki változó nem lesz igaz. Majd ezután megfelelő üzenetet írunk ki
attól függően, hogy a játékos eltalálta a rejtett szót vagy feladta a játékot.

Absztrakt program

A résztevékenységet megoldó programot annak utófeltétele alapján könnyen


elkészíthetjük.

darab:=darab+1

ki := (tipp = rejt tipp = ”x”)

ki

tipp := rejt

i = 1 .. rejt SKIP

tipp[i] 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

118
megelőző beolvasásából (az aktuális tippet vagy a kilépési szándékot kell itt
megkérdeznünk) valamint az azt követő (a próbálkozás eredményének)
kiírásból áll. Ezt ágyazzuk be a külső szintbe, amely kezdeti értékadásokkal
kezdődik (a rejtvény bekérése, a próbálkozások darabszámának lenullázása, a
kilépést jelző logikai változó hamisra állítása), majd a belső szint ciklikusan
ismételt végrehajtása következik, végül az eredmény kiírására kerül sor.

Belső szint implementálása

Az absztrakt program kódolása önmagában nem jelenthet gondot. A


vezérlési szerkezetek kódolását ugyanis már ismerjük, legfeljebb a
karakterláncok (sztringek) használatával kell megbirkóznunk.
Egy karakterlánc hasonlít egy karaktereket tartalmazó tömbre,
amennyiben egy sztring i-edik karakterére a tömböknél látott indexeléssel
hivatkozhatunk. A tipp kiértékelését végző ciklus kódolásához szükség van a
karakterlánc hosszának ismeretére (size()).
for(int i=0; i<(int)rejt.size(); ++i)

if(tipp[i] != rejt[i]) tipp[i] = '.';

A tipp := rejt értékadás megvalósításához egyfelől le kell vágnunk a


tipp hosszából, ha az túlnyúlna a rejtvény hosszán, másfelől meg kell
nyújtani, ha rövidebb lenne, mint a rejtvény. Egy sztringhez hozzáfűzhető egy
másik sztring, kereshetünk benne adott részsztringet vagy karaktert,
kivághatunk belőle egy részt.
A substr() függvény segítségével egy karakterláncnak egy részét,
megadott pozíciótól kezdődő megadott darabszámú karakterét tudjuk
kivágni. A tipp.substr(0,n) a tipp karakterláncnak n hosszú elejét
(nullával kezdődik egy sztring indexelése) kapjuk meg. Ha az n hosszabb, mint
a tipp hossza, akkor a visszakapott rész-sztring maga a tipp lesz.
tipp = tipp.substr(0, (int)rejt.size());

Egy sztringhez a + operátorral fűzhetünk hozzá másik sztringet. Például


str+"bővítmény". Az str=str+"bővítmény" értékadást

119
str+="bővítmény" alakban rövidíthetjük. Egyformán működik az
str=str+"y" és az str=str+’y’, mert a sztringhez a + operátorral
nemcsak sztringeket, hanem karaktereket is hozzá lehet fűzni. Általában
azonban meg kell különböztetnünk az egyetlen karakterből álló
karakterláncot a karaktertől. Ügyeljünk arra, hogy a sztringeket idézőjelek
közé, a karaktereket aposztrófok közé kell írni, és ne lepődjünk meg, hogy az
’y’=="y" vizsgálat szintaktikailag helytelen. A nyújtás során olyan
karaktereket (például pontokat) kell hozzáírnunk a tipphez, amelyek biztosan
nem egyeznek meg a rejtvénybeli adott pozíciójú karakterekkel hiszen azokat
– mivel egyáltalán nem szerepelt a tippünk ezen pozícióin karakter – nem
találtuk el a rejtvényből.
for(int i=(int)tipp.size();

i<(int)rejt.size();++i)

tipp += '.';

Nézzük most meg egyben a belső szint kódját.

// Játékos szándéka

cout << "\nHa kilép, írjon be egy x-et. "

<< "Ha nem, tippeljen!\n";

cout << "Tipp = "; cin >> tipp;

if(tipp != "x") ++darab;

ki = tipp == rejt || "x" == tipp;

if(!ki){

// |tipp| := |rejt|

tipp = tipp.substr(0, (int)rejt.size());

120
for(int i=(int)tipp.size();

i<(int)rejt.size(); ++i) tipp += '.';

// tipp kiértékelése

for(int i=0; i<(int)rejt.size(); ++i)

if(tipp[i] != rejt[i]) tipp[i] = '.';

cout << endl << darab << ". próbálkozás = "

<< tipp << endl;

Az absztrakt program kódját el kell látni megfelelő beolvasás és kiírás


részekkel is. Az előfeltételben szereplő bemenő változók közül itt az absztrakt
program előtt csak a játékos soron következő tippjének beolvasását kell
elvégezni, a rejtvényt csak a legelső próbálkozás előtt, a külső szinten
olvassuk be, és ugyanott nullázzuk le a próbálkozások darabszámát is, amely
minden menetben eggyel nő. A tipp kiértékelése után pedig – ha nem akart a
játékos kilépni a játékból – megjelenítjük, hogy hányadik próbálkozásnál
járunk, és kiírjuk a kiértékelt tippet.

Külső szint implementálása

A belső szint kódját egyszer mindenképpen, de többnyire sokszor egymás


után kell végre hajtani. Ezért ezt egy ciklusba ágyazzuk, ráadásul úgy, hogy
annak a magja egyszer biztosan végrehajtódjon. Sok programozási nyelvben
található olyan ciklusutasítás, amely éppen így működik; ezt hívják hátul
tesztelő ciklusnak.
A hátul tesztelős ciklust C++ nyelvben a do-while utasítással írhatjuk le.
A do{ciklusmag}while(feltétel) az alábbi programszerkezetnek felel

121
meg. A do-while utasítás alkalmazásával a ciklusmagot csak egyszer kell
kódolni.

ciklusmag

feltétel

ciklusmag

E ciklus előtt azonban ne feledkezzünk meg a rejtvény beolvasásáról és


a próbálkozások darabszámának nullára állításáról, utána pedig az eredmény
megjelenítéséről.

cout << "Adja meg a rejtett szót: ";

cin >> rejt;

int darab = 0;

do{

// Belső szint

}while(!ki);

if(tipp == rejt)

cout << "\nGratulálok! " << darab

<< ". tippel nyert!\n";

else if ("x" == tipp)

cout << "\nÖn vesztett! Majd legközelebb.\n";

122
123
Tesztelés

A fekete doboz teszteseteket két csoportba soroljuk. Egyrészt teszteljük a


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

A fehér doboz tesztelés a fenti tesztesetek lefedik, így újabb


tesztesetekre nincs szükség.

124
Teljes program

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

#include <iostream>

#include <string>

using namespace std;

int main()

// Rejtvény beolvasása

string rejt;

cout << "Adja meg a rejtett szót: ";

cin >> rejt;

int darab = 0;

// Próbálkozások a rejtvény kitalálására

string tipp;

bool ki;

do{

// Játékos szándéka

cout << "\nHa kilép, írjon be egy x-et. "

<< "Ha nem, tippeljen!\n";

cout << "Tipp = "; cin >> tipp;

if(tipp != "x") ++darab;

ki = tipp == rejt || "x" == tipp;

125
if(!ki){

// |tipp| := |rejt|

tipp = tipp.substr(0, (int)rejt.size());

for(int i=(int)tipp.size();

i<(int)rejt.size(); ++i) tipp += '.';

// tipp kiértékelése

for(int i=0; i<(int)rejt.size(); ++i)

if(tipp[i] != rejt[i]) tipp[i] = '.';

cout << endl << darab << ". próbálkozás = "

<< tipp << endl;

}while(!ki);

// Eredmény kiírása

if(tipp == rejt)

cout << "\nGratulálok! " << darab

<< ". tippel nyert!\n";

else if ("x" == tipp)

cout << "\nÖn vesztett! Majd legközelebb.\n";

return 0;

126
C++ kislexikon

vektor konstans const Element v[] = { … , … , };

definiálása const int size =

sizeof(v)/sizeof(v[0]);

konstans const int maxsize = 100;

méretű Element v[maxsize];

int size;

… // size értéke: 0≤size≤maxsize

rögzített int size;

méretű … // 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 for(int i=0; i<size; ++i){

feltöltése cin >> v[i];

mátrix konstans const int maxi = 100, maxj = 100;

definiálása méretű Element a[maxi][maxj];

int sizei, sizej;

127
… // sizei értéke: 0≤sizei≤maxi

// sizej értéke: 0≤sizej≤maxj

rögzített int sizei, sizej;

méretű … // sizei értéke: 0≤sizei

// sizej értéke: 0≤sizej

Element a[sizei][sizej]; //NEM C++

vector<> #include <vector>

(téglalap …
mátrix)
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 for(int i=0; i<sizei; ++i){

feltöltése for(int j=0; i<sizej; ++j){

cin >> a[i][j];

128
4. Konzolos be- és kimenet

Absztrakt programjaink tervezésekor feltételezzük, hogy a bemenő adatok


értékeit a szükséges ellenőrzések után már az úgynevezett bemenő változók
tartalmazzák, az eredmény pedig az eredmény változókba kerül. Egy
futtatható program azonban nem állhat kizárólag az absztrakt program
kódjából, hiszen olyan részeket is tartalmaznia kell, amelyek a bemenő
adatokat a felhasználótól a bemenő változókba juttatják el, közben elvégzik
az ellenőrzéseket, és az eredményt a felhasználó számára is érthető
(olvasható) formában jelenítik meg.
Annak, hogy a felhasználó adatokat adjon meg egy programnak és
adatokat kapjon tőle, több módja is van. Lényegesen korlátozza a
lehetőségeinket az a tény, hogy ebben a könyvben nem grafikus felhasználói
felületet használunk begépelt adatok beolvasásához illetve az eredmény
kiírásához, hanem vagy konzolablakos technikát, vagy szöveges
állományokat. (Ez utóbbival a következő fejezetben foglalkozunk.) Ez egy
szöveges alapú kommunikáció a felhasználó és a program között. Legkisebb
egységei a karakterek, amelyeket sorban egymás után lehet csak beírni vagy
megjeleníteni, és nincs lehetőség egy korábban be- vagy kiírt karakterhez
visszatérni, azt módosítani.

Implementációs stratégia

Egy alkalmazás be- és kimeneti (input-output) tevékenysége nem kizárólag a


bemenő adatok beolvasásából és az eredmény adatok kiírásából áll. Nagyon
fontos, hogy ez olyan körítésben, olyan tájékoztató üzentekkel együtt
történjen meg, amelyből a felhasználó (nem a programozó) tudni fogja, hogy
mikor mit kell csinálnia, mit vár tőle a program, mit kaphat ő a programtól. A
felhasználóbarát szoftver fontos ismérve ez a fajta öndokumentáltság, azaz
az a képesség, hogy a program használata annak működése közben
érthetővé, megtanulhatóvá váljon. Ennek kialakítása nem implementációs
kérdés, hanem az ember-gép kapcsolat megértésének és tervezésének, a
szoftverergonómiának a része, amelyet az implementáció keretében
kódolunk hozzá az alkalmazáshoz. Ez többek között meghatározza, hogy az
adatokat és üzeneteket milyen formában, milyen adagokban közvetítse a

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

1. Beolvasott érték formátumának ellenőrzése


2. Beolvasott érték és az azt fogadó bemenő változó típusának
(az állapottérnek) egyeztetése
3. A bemenő változókra megfogalmazott előfeltétel vizsgálata

5-1. ábra. Adatellenőrzés szintjei

A második szint az adatoknak a feladat által elvárt feltételekkel való


egybevetése. Ez egyfelől az állapottérrel való megfelelést, másfelől az

130
előfeltétel vizsgálatát jelenti. Előbbire példa az, amikor egy bemenő változó a
feladat specifikációja szerint természetes szám, de az adott programozási
nyelv ezt csak egy integer (egész típusú) változóba képes beolvasni. Ilyenkor
ellenőrizni kell, hogy a beolvasott érték nem negatív-e, azaz tényleg
természetes szám-e. Ezt követően vizsgálni kell, hogy a beolvasott érték
kielégíti-e az előfeltételben vele szemben támasztott követelményeket.
Természetesen az ellenőrzés különböző szintjeit nem kell a kódban
szétválasztani, azok összevonhatók. Érdemes figyelni arra, ha több adat
bekérésére is sor kerül egymás után, akkor minden beolvasás után külö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.

1. Figyelmeztető üzenet után a program leállítása


2. Figyelmeztető üzenet után az adat ismételt bekérése

4-2. ábra. Hibás adat lekezelésének technikái

Tovább fokozható a szoftver használatának kényelme az alkalmazás


végtelenítésével, amikor is a program befejeződésekor megkérdezzük a
felhasználót, hogy akar-e újabb futtatást végezni. Így nem kell újraindítani az
alkalmazást, csak a végén az „Igen. Folytatom.” választ megadni. Ennek
továbbfejlesztéseként is felfogható az, amikor egy futtatási ciklusban

131
nemcsak arról dönthet a felhasználó, hogy akarja-e folyatatni a futtatást,
hanem arról is, hogy azt milyen feltételek mellett akarja elvégezni, esetleg
egy több funkciós programnál kiválaszthatja, melyik funkciót akarja
kipróbálni. Itt egy úgynevezett menü implementálására van szükség. Ha az
egész alkalmazásra vonatkozik a menü, amely ráadásul végtelenítve van,
akkor mindig szerepeljen a menüpontok között a befejezést választó eset.
Még az eredetileg helyes absztrakt programot is félre lehet kódolni,
ezért utólag azt is ellenőrizni, tesztelni kell, de az input-output
tevékenységért felelős kód megfelelő működését csak így lehet ellenőrizni,
hiszen a tervezés nem tér ki ennek részletezésére.

Nyelvi elemek

A futtatási környezetek mindig biztosítanak valamilyen karakteres


üzemmódú soros elérésű konzolablakos input-output lehetőséget. Ennek
használata során a program különféle típusú értékeket vár a felhasználótól,
és küld a felhasználó felé. Bevitelnél az értékeket rendszerint a billentyűzet
segítségével gépelhetjük be úgy, hogy az értéket kifejező karaktersorozatot
adjuk meg. Beviteli tevékenységünket az <enter> billentyű megnyomásával
fejezzük be. A begépelt karakterek a konzolablakban is megjelennek az
aktuális pozíción. A kiírt értékek az őket leíró karaktersorozat formájában
jelennek meg a konzolablakban. A konzolablakba történő íráskor adott egy
aktuális sor és azon belül egy aktuális pozíció, amely azt mutatja, hogy a
kiírandó következő karaktert hol kell megjeleníteni. Egy karakter kiírása után
az aktuális pozíció a soron következő pozíció lesz, sor vége esetén ez a
következő sor eleje.

>>
cin változó

cout <<
kifejezés

132
4-3. ábra. Szabványos input/output

Korszerű, több programozási nyelv által használt input-output


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

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

134
Adatfolyamok

Az adatfolyamok (stream) olyan objektumok, amelyek a hozzájuk


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

A hátul tesztelő ciklus a ciklusmagot egyszer mindenképpen


végrehajtja, és csak azt követően ellenőrzi a ciklus feltételt. Tulajdonképpen
egy olyan szekvencia, amelyiknek első tagja a ciklusmag, második tagja pedig

135
egy szokásos (elöl tesztelő) ciklus, amelyiknek a ciklus magja azonos a
szekvencia első tagjával.
A C++ nyelvben a hátul tesztelős ciklust a do-while utasítással adhatjuk
meg. Fontos megemlíteni, hogy a while(ciklusfeltétel) részben a
zárójelezett kifejezés azt a logikai értéket állítja elő, amelynek igaz értéke
esetén a do és while közötti utasítást (vagy utasítás blokkot) meg kell
ismételni, hamis értéke esetén a ciklus utáni utasításra kerül a vezérlés. (Ez
éppen a fordítottja a Pascal nyelvbeli repeat-until utasítás működésének.)

ciklusmag do{

ciklusfeltétel ciklusmag

ciklusmag }while(ciklusfeltétel)

4-4. ábra. Hátul tesztelő ciklus

Ugyancsak hátul tesztelő ciklussal érdemes egy alkalmazás


„végtelenítését” kódolni. (Erre a 8. feladatnál már láttunk példát.) C++
nyelven ezt általában úgy érhetjük el, hogy a main függvény törzsét az utolsó
return 0 utasítás kivételével egy do-while utasításba ágyazzuk, és a
ciklusmag végén megkérdezzük a felhasználót, akarja-e megismételni a
program futását.
int main()

char ch;

do{

...

cout << "Folytatja? (I/N): "; cin >> ch;

136
}while(ch != 'n' && ch != 'N');

return 0;

Ennek egy továbbfejlesztett változata az, amikor egy menü


segítségével vezéreljük a program futását. Erre a 10. feladat majd mutat
példát.

A fenti eseteken kívül a do-while ciklus alkalmazását programjainkban


nem ajánljuk.
A C++ nyelvben a kiírás a cout << kifejezés vagy cout <<
kifejezés1 << kifejezés2 utasításokkal végezhető. A kiírás a
konzolablak azon pontján kezdődik, ahol az előző kiírás befejeződött. Amikor
egy sor betelik, a kiírás automatikusan a következő sorban folytatódik. A
kiírás során szívesen élünk olyan lehetőségekkel, amellyekkel a kiírt értékeket
felhasználóbarát formában tudjuk elhelyezni a konzolablakban. Már egy
szóköz kiírásával jól szeparálhatóak a megjelenített adatok, de használhatunk
pozícionált kiírást. A cout << setw(10)<< kifejezés például a
következő 10 pozíción helyezi el a kifejezés értékét, attól függően balra vagy
jobbra tömörítve, hogy milyen típusú a kiírt érték, illetve hogyan állítottuk be
ezt az opciót előzőleg. (Erről illetve az ehhez hasonló opciókról a következő
bekezdésben írunk részletesen.) A tabulálást a cout << ’\t’ kiírás, a
soremelést a cout << endl vagy cout << ’\n’ biztosítja.
Végezetül ki kell térnünk a beolvasással kapcsolatos igen kellemetlen
jelenségre. Ha például egy egész számot akarunk beolvasni az eddig
alkalmazott
int n; cin >> n;

technikával, de nem számformájú karaktereket gépelünk be, akkor a


beolvasás elromlik. Az nem meglepő, hogy az n változó ilyenkor nem kap
értéket, de az már igen, hogy az ezt követő összes beolvasás sem fog már
helyesen működni.

137
Ezt a jelenséget kétféleképpen tudjuk kezelni. Az egyik lehetőség az,
hogy egy beolvasást követően rákérdezünk arra, hogy a beolvasásnak milyen
az állapota. A cin.fail() függvény hamis értéket ad vissza, ha a beolvasás
„elromlott”. Ebben az esetben hibaüzenetet küldhetünk a felhasználónak,
újra bekérhetjük az adatot. Ahhoz azonban, hogy az ezt követő beolvasások
sikerüljenek, ki kell törölnünk a beolvasás „emlékeit”. Egyrészt visszaállítjuk a
beolvasás állapotát úgy, hogy a fail() függvény ne jelezzen továbbra is
hibát (cin.clear()), másrészt kiürítjük (getline(cin,tmp)) a beolvasás
során bevitt, de fel nem dolgozott karaktereket a bemeneti adatfolyamból.
Fontos, hogy ezt a kiürítést megelőzze a cin.clear() utasítás.

Manipulátorok, formátumjelzők C++ nyelvben

A formázott kimenetet a manipulátorok és a formátumjelző bitek


(flag) segítségével vezérelhetjük.
A formátumjelző bitek a kiírás formáját határozzák meg. Ilyen például
a scientific (lebegőpontos alak), fixed (fixpontos alak – alapértelmezett),
right, left (jobbra ill. balra tömörítés), dec, hex, oct (számrendszer, dec az
alapértelmezett), showpoint (tizedespont mindig látszódjon), showpos
(pozitív előjel is látszódjon), uppercase (csupa nagybetű), boolalpha (logikai
érték kiírásához). Ezeket a tulajdonságokat a setf() illetve unsetf()
függvények segítségével kapcsolhatjuk be illetve ki. E függvények
argumentumában | jellel elválasztva sorolhatjuk fel a szükséges
formátumjelző biteket. Némelyik tulajdonság alapértelmezett módon be van
kapcsolva. Minden formátumjelző bit elé az ios:: minősítést kell írni. Ha
például a számokat lebegőpontos alakban, balra tömörítve szeretnénk kiírni
úgy, hogy a tizedespont és az előjel minden esetben látszódjon, akkor ezt a
cout.setf(ios::scientific|ios::showpoint|ios::showpos)
utasítással állíthatjuk be.
A manipulátorokat a << operátorral kell az adatfolyamra elküldeni, és a
következő kiírásra van hatással. Az endl kiírás egy soremelést eredményez,
a setw(int w)-vel a következő kiírás számára fenntartott szélességet,
pozíció számot adhatjuk meg. Egy valós szám törtjegyeinek számát például a
setprecision(int p), kitöltő karaktereket a setfill(char c) segítségével

138
idézhetjük elő. A paraméterrel rendelkező manipulátorok eléréséhez az
iomanip csomagra van szükségünk. Számos formátumjelző bitként
megadható tulajdonság manipulátorként is be- illetve kikapcsolható
(showpos / noshowpos).
Ez a technika nemcsak a kiíráshoz használt I/O adatfolyamra
alkalmazható. Formátumjelző bitekkel és manipulátorokkal szabályozható a
beolvasás is. Természetesen más tulajdonságok állíthatóak be a
beolvasásnál, mint a kiírásnál. Csak beolvasásnál van értelme például a
vezető üres karakterek átugrására (cin >> ws >> …), csak kiírásnál a csupa
nagybetűs szövegként való megjelenítésre (cout << uppercase << …), a
logikai értékek kezelését előíró tulajdonság (boolalpha) viszont
beolvasásnál is, kiírásnál is használható.

int n; cin >> n;

if(cin.fail()){

cout << "Hiba!\n";

cin.clear();

string tmp; getline(cin,tmp);

Nemcsak a cin objektumra lehet ilyen hasznos függvényeket


(fail(), clear(), getline()) meghívni, hanem a cout objektumra is. A
cout objektumra hívható függvényekkel a kiírás formáját tudjuk
befolyásolni. A konkrét lehetőségeket az alábbi alkalmazásokban mutatjuk
majd be.
A másik megoldás az, ha az adatbevitelre szánt karaktereket először
mindig egy sztring típusú változóba olvassuk be, mert ez nem okozhat hibát,
majd ellenőrizzük, hogy a sztringbe bekerült karakterek megfelelnek-e,
például valóban egy egész számot írnak-e le. Ehhez felhasználjhatuk a C

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

int n = atoi(str.c_str());

if (0 == n && str != "0"){

cout << "Hiba!\n";

140
Különféle karakterláncok

A C programozási nyelv a karakterláncokat olyan byte sorozatként


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

141
9. Feladat: Duna vízállása

Vízügyi szakemberek egy adott időszakban rögzítették a Duna Budapesten


mért vízállásait. Készítsünk hisztogramot a mért adatokról, miután
ellenőriztük az adatok helyességét.
Ez a feladat különleges abból a szempontból, hogy tényleges számítást,
tehát az előző fejezetek feladatainak megoldásainál előállított absztrakt
megoldó programot nem igényel. A feladat ugyanakkor határozott
kívánságot fogalmaz meg a kiírással szemben. Ezért az absztrakt program itt a
korábbiaktól eltérő szerepkörben jelenik meg, a kiírás tervét hordozza.
Tipikus határesettel van tehát dolgunk. Eddig a kiírás részekkel kizárólag az
implementációban foglalkoztunk, de most ezt is részletesen specifikálni kell,
részben absztrakt programot kell hozzá készíteni.

Specifikáció

A feladatnak csak bemenő változója van, ennek megfelelően célfeltétele


sincs.
A = ( v : ℤn )
Ef = ( v=v’ i [1..n]: v[i] ≥ 0 )

A kiírás során egy hisztogramot kell megjelenítenünk. A hisztogram egy


olyan diagram, amelynek alapvonalára merőlegesen egymás után annyi
darab hasábot rajzolunk, ahány mérési adatunk van. A hasábok hossza a
mért értékekkel arányos, rá is lehet írni minden hasábra a hozzátartozó
értéket, a hasáb alá az alapvonalra pedig a mérési eseménnyel kapcsolatos
információt (sorszámot, esetleg dátumot).
A hisztogramot egy konzolablakban úgy kényelmes megjeleníteni, ha
az alapvonalát függőlegesen, az ablak baloldalán képzeljük el. Így egy sor, a
hisztogram egy hasábját tartalmazza, és ahány hasábból áll a hisztogram
annyi sora a lesz a kiírásnak. (A szokásos elrendezés az, amikor a hisztogram
alapvonala vízszintes. Ebben az esetben azonban a konzolablak szélessége
korlátozná a kiírható hasábok számát.) Mivel erősen korlátozottak a
lehetőségeink a hisztogram megjelenítésére, ezért az alapvonalat ne is

142
rajzoljuk ki, a hasábokat pedig megfelelő számú egymás után írt ’*’
karakterrel helyettesítsük. Írjuk ki a hasáb mellé, hogy az hányadik méréshez
tartozik, és jelenítsük meg a mért értéket is.

Absztrakt program

A hisztogram arányos megjelenítéséhez szükség van a megjelenítendő


legnagyobb értékre.
A = ( v : ℤn, max : ℤ )
Ef = ( v=v’ n>0 )
n
Uf = ( v=v’ max = max v[i])
i 1

max := v[1]
i = 2 .. n
v[i]>max
max := v[i] SKIP

Elég csak a maximális értéket kiszámolni, nincs szükség arra az indexre,


amelyik a maximális elemet jelöli. Mivel a maximum kiválasztás csak akkor
értelmes, ha van legalább egy mérés, ezért a fenti programot egy olyan
elágazásba ágyazzuk, amelyik vizsgálja az n>0 feltételt. Ha ez nem teljesül,
akkor nem rajzolunk hisztogramot.
A maximális értékhez tartozó hasábot fogjuk a lehető leghosszabbra
rajzolni, a többit pedig ehhez mérten arányosan jelenítjük meg.
Magának a hisztogramnak a rajzolása annyiban tér el egy tömb
elemeinek kiírásától, hogy egy tömbelem (index és érték) kiírását mindig új
sorba tesszük, és ott megfelelő számú ’*’ karaktert is elhelyezünk. Ehhez
ismernünk kell a konzolablak szélességét azaz azt az m értéket, amely azt
jelöli, hogy legfeljebb mennyi csillag fér el a konzol ablakban vízszintesen:
ennyi csillaggal fogjuk a legnagyobb hasábot megjeleníteni. A tömb egy

143
tetszőleges v[i] értékéhez tartozó hasábban (v[i]/max)*m egészrésze számú
csillagot kell majd kiírnunk.

i = 1..n
i kiírása

j = 1.. (v[i]/max)*m
’*’ kiírása
v[i] kiírása

Implementálás

Beolvasás

A vector<int> segítségével definiált tömb feltöltése lényegében


megegyezik az előző fejezetben látottakkal, de most „bolond-biztossá”
tesszük az egész számok beolvasását és hibás adatbevitel esetén egy
hibaüzenet kiírása után azonnal leállítjuk a programot. Ezt alkalmazzuk a
tömb elemszámának beolvasásánál és a tömb elemeinek beolvasásánál is.

cout << "Adja meg a tömb hosszát (1 <= n )";

int n;

cin >> n;

if(cin.fail() || n<=0){

cout << "Hiba!\n";

return 1;

144
vector<int> v(n);

cout << "Adja meg a tomb elemeit!\n";

for(int i=0; i<n; ++i){

cout << i + 1 << ". elem: ";

cin >> v[i];

if(cin.fail() || v[i]<0){

cout << "Hiba!\n";

return 1;

Kiírás

Klasszikus értelemben vett számításrész most nincsen, viszont a kiírás módját


az absztrakt program leírja. Ez egyrészt egy (az előző fejezetben már látott)
maximum kiválasztásból áll, és egy formázott kiírást végző részből.

int ind = 0, max = v[0];

for(int i=1; i<n; ++i){

if (v[i]>max){

ind = i; max = v[i];}

int m = 30;

145
cout << endl;

for(int i=0; i<n; i++){

cout << setw(5) << i+1 << " ";

for(int j=0; j<(v[i]/max)*m; i++){

cout << setw(2) << "*";

cout << v[i] << endl;

A soron következő formázott kiíráshoz használtuk a setw(int w)


manipulátort, amely azt állítja be, hogy a soron következő kiírás w pozíción
(helyen, szélességben) történjen. Szám kiírása esetén alapértelmezés szerint
a megadott mező jobbszéléhez illesztve fog a szám értéke megjelenni,
karakter, karakterlánc esetén a balszéléhez illesztve. Ennek a manipulátornak
a használatához szükség van az iomanip könyvtárra. (#include
<iomanip>). A manipulátorokat a << operátorral kell a kimeneti
adatfolyamra elküldeni. A setw()-hez hasonló manipulátorral már korábban
is találkoztunk: ilyen volt a soremelésre használt endl, csak ahhoz még nem
kellett az iomanip könyvtár.

Tesztelés

Ez a program elsősorban fehérdoboz módszerrel tesztelhető. Négy szakaszra


bontható: a tömb definiálása, tömb feltöltése, tömb maximális elemének
kiválasztása és a kiírás. Az első három fázis tesztelése a 6. feladatáéval
azonos módon történik.
A kiírás rész tesztelése az eredmény megjelenési formájának
ellenőrzéséből áll. Különböző hosszú és értékű tömb segítségével
próbálhatjuk ki, hogy a kiírás egymásba ágyazott ciklusai megfelelően
működnek-e.
1. Külső ciklus magja egyszer sem fut le.

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

147
Teljes program

#include <iostream>

#include <iomanip>

#include <vector>

#include <string>

using namespace std;

int main()

// Tömb definiálása

cout << "Adja meg a tömb hosszát (1 <= n )";

int n; cin >> n;

if(cin.fail() || n<=0){

cout << "Hiba!\n";

return 1;

vector<int> v(n);

cout << "Adja meg a tomb elemeit!\n";

for(int i=0; i<n; ++i){

cout << i + 1 << ". elem: ";

cin >> v[i];

if(cin.fail() || v[i]<0){

cout << "Hiba!\n";

148
return 1;

// Maximumkeresés

int ind = 0, max = v[0];

for(int i=1; i<n; ++i){

if (v[i]>max){

ind = i; max = v[i];

// Hisztogram rajzolása

int m = 30;

cout << endl;

for(int i=0; i<n; i++){

cout << setw(5) << i+1;

for(int j=0; j<(v[i]/max)*m; i++){

cout << " *";

cout << v[i] << endl;

149
10. Feladat: Alsóháromszög-mátrix

Két, valós számokat tartalmazó alsóháromszög mátrixot (ez olyan mátrix,


amelynek a főátlója feletti elemei mind nullák) szorozzunk össze! Miután a
mátrixokat beolvastuk a billentyűzetről és elvégeztük az összeszorzásukat,
egy menüből válasszuk ki, hogy milyen formában akarjuk a mátrixot
megjeleníteni: kiírjuk-e a főátló feletti értékeket vagy csak a helyüket
jelezzük; az értékek lebegőpontos formában vagy fixpontos formában
jelenjen meg, illetve hogy a kiírás pontossága milyen legyen.

Specifikáció

A feladat két alsóháromszög mátrix összeszorzása. Az általános mátrixszorzás


képlete: i [1..n]: j [1..n]: c[i,j] = ). Alsóháromszög
mátrixok esetén ezt a képletet egyszerűsíteni lehet. Egyrészt az eredmény
mátrixnak csak az alsóháromszög részét kell kiszámolnunk, mivel
alsóháromszög mátrixok szorzata is alsóháromszög alakú. Másrészt a
számolás során a biztosan nulla értékű szorzatokat (amikor valamelyik
mátrixnak a jobb-felső területéről származó értékkel szorzunk) felesleges
kiszámítanunk: i [1..n]: j [1..i]: c[i,j] = )
Érdemes azt is észrevenni, hogy egy alsóháromszög mátrix főátló
feletti nulla értékeit nem kell külön eltárolni. Az alsóháromszög részbe eső
elemek elférnek egy n(n+1)/2 elemű egydimenziós tömbben. Ha
sorfolytonosan helyezzük ebbe a tömbbe a mátrix alsóháromszög részének
elemeit, akkor az i-dik sor j-dik eleme a tömb i(i–1)/2+j -dik eleme lesz. Az
állapottérbe az a, b, c : ℝ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]) ) )

150
Absztrakt program

i = 1 .. n
j = 1 .. i
c[i(i–1)/2+j] := 0.0
k = j .. i
c[i(i–1)/2+j] := c[i(i–1)/2+j] +
a[i(i–1)/2+k] * b[k(k–1)/2+j]

Implementálás

Ügyeljünk arra, hogy a C++ nyelv 0-tól kezdődően indexeli a tömböket, ezért
az absztrakt programban használt tömbindexelés mindenhol helyett eggyel
csökkentett indexet használjunk. Például a mátrix i,j-edik elemére történő
hivatkozáskor az i(i–1)/2+j helyett i(i–1)/2+j–1-et.

Beolvasás

Miután beolvastuk a mátrixok közös méretét (n), egy mátrix beolvasása,


amely valójában egy egydimenziós tömb feltöltése, az alábbi szerint alakul:

int n;

cin >> n;

vector<double> a(n*(n+1)/2);

for(int i=1; i<=n; ++i){

for(int j=1; j<=i; ++j){

cout << "a[" << i << "," << j << "]= ";

151
cin >> a[(i-1)*i/2 + j - 1];

A végleges változatban azonban minden beolvasás köré építsünk egy


hibaellenőrző hátul-tesztelős ciklust, amely mindaddig újra és újra adatot
kér, amíg nem adjuk meg azt helyesen.

Számolás

A szorzás az absztrakt program hű másolata. Felhasználjuk azt, hogy a


vector típusú adat létrehozásakor meg lehet adni egy a vektor elemeit
kezdetben kitöltő értéket.

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.

152
Az egyéb beállítási lehetőségeket a fejezet végén található C++ kislexikonban
olvashatjuk.

cout.setf(ios::fixed|ios::left|ios::showpoint);

cout << setprecision(2);

for(int i=1; i<=n; ++i){

for(int j=1; j<=n; ++j){

if(i>=j) cout << setw(10) << c[(i-1)*i/2+j-1];

else cout << setw(10) << 0.0;

cout << endl;

A második esetben a főátló feletti elemeknek csak a helye látszik, a


nulla értékek nem, a többi szám pedig lebegőpontos formában 1 tizedesjegy
pontossággal, mindig mutatva az előjelet, tíz pozíción jobbra igazítva.

cout.setf(ios::scientific|ios::showpos);

cout << setprecision(1);

for(int i=1; i<=n; ++i){

for(int j=1; j<=i; ++j){

cout << setw(10) << c[(i-1)*i/2+j-1];

cout << endl;

153
A fenti két programrészt az alábbi kódba ágyazzuk.

char ch;

do{

cout << "Kiírás módja:\n";

cout << "1 - Főátló feletti elemekkel, "

<< "fixpontosan, balra igazítva, "

<< "2 tizedesjegy pontossággal\n";

cout << "2 - Főátló feletti elemek nélkül, "

<< "lebegőpontosan, előjelesen, " <<

<< "1 tizedesjegy pontossággal\n";

cout << "Válasszon: ";

int k; cin >> k;

switch(k){

case 1: // első fajta kiírás

break;

case 2: // második fajta kiírás

break;

default:;

cout << "Folytatja? (I/N): "; cin >> ch;

154
}while(ch != 'n' && ch != 'N');

Ez egy „végtelenített” működést valósít meg. Az ismétlődő rész egy


kételemű menüt ajánl fel, és a megfelelő menüpont kiválasztásával a kiírás
formáját lehet meghatározni. Ehhez a C++ nyelv speciális elágazó utasítását,
a switch() utasítást használjuk. Az utasítással egy kifejezést adunk meg (itt
ez egyszerűen a k), amelynek lehetséges értékeit sorolják fel az esetek
(case). Minden esethez tartozó program végén a break utasítás áll,
különben a vezérlés a következő eset programjára kerül, nem pedig a switch
után. Ha a kifejezés olyan értéket vesz fel, amelyik nem szerepel esetként,
akkor a switch utasítás nem csinál semmit. Ezt emeli ki az üres utasítású
default ág, amelyre ugyan nincs szükség, de explicit módon való kiírása a
biztonságos kód kialakítását támogatja.

Tesztelés

A feladat szempontjából készített fekete doboz tesztesetek:

Érvényes tesztesetek:

1. Null-mátrixszal való (jobbról, balról) szorzás (Eredmény: 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:

155
1. Hibás méret (<=0) beírása.

A programkód alapján készített tesztesetek:

1. Mátrix méretének beolvasása és ellenőrzése: amikor a ciklus egyszer


fut le (jó adat), többször fut le (először rossz adat, a végén jó).
2. Mátrixok feltöltését és a mátrixok szorzását végző ciklusok
ellenőrzése (fekete doboz tesztestek)
3. A kiírás tesztelése (kétféle kiírás ciklikusan ismételve)

156
Teljes program

#include <iostream>

#include <iomanip>

#include <vector>

#include <string>

using namespace std;

int main()

// Mátrix méretének beolvasása

int n;

bool error;

do{

cout << "Adja meg a mátrixok méretét: ";

cin >> n;

if(error = cin.fail() || n<1){

cout << "Pozitív egész szám legyen!\n";

cin.clear();

string tmp; getline(cin,tmp);

}while(error);

157
// Első mátrix beolvasása

cout << "Első mátrix:\n";

vector<double> a(n*(n+1)/2);

for(int i=1; i<=n; ++i){

for(int j=1; j<=i; ++j){

do{

cout << "a[" << i << "," << j << "]= ";

cin >> a[(i-1)*i/2+j-1];

if(error = cin.fail()){

cout << "Valós szám legyen!\n";

cin.clear();

string tmp; getline(cin,tmp);

}while(error);

// Második mátrix beolvasása

158
cout << "Második mátrix:\n";

vector<double> b(n*(n+1)/2);

for(int i=1; i<=n; ++i){

for(int j=1; j<=i; ++j){

do{

cout << "b[" << i << "," << j << "]= ";

cin >> b[(i-1)*i/2+j-1];

if(error = cin.fail()){

cout << "Valós szám legyen!\n";

cin.clear();

string tmp; getline(cin,tmp);

}while(error);

// Mátrix-szorzás

vector<double> c(n*(n+1)/2, 0.0);

for(int i=1; i<=n; ++i)

for(int j=1; j<=i; ++j)

for(int k = j; k<=i; ++k)

c[(i-1)*i/2+j-1]+=

a[(i-1)*i/2+k-1]*b[(k-1)*k/2+j-1];

//Kiírás kétféleképpen

char ch;

159
do{

cout << "Kiírás módja:\n";

cout << "1 - Főátló feletti elemekkel, "

<< "fixpontosan, balra igazítva, "

<< "2 tizedesjegy pontossággal\n";

cout << "2 - Főátló feletti elemek nélkül, "

<< "lebegőpontosan, előjelesen, " <<

<< "1 tizedesjegy pontossággal\n";

cout << "Válasszon: ";

int k;

do{

cout << "Adja meg a mátrixok méretét: ";

cin >> k;

if(error = cin.fail() || n<1){

cout << "Pozitív egész szám legyen!\n";

cin.clear();

160
string tmp; getline(cin,tmp);

}while(error);

switch(k){

case 1: cout.setf(ios::fixed|

ios::left|ios::showpoint);

cout << setprecision(2);

for(int i=1; i<=n; ++i){

for(int j=1; j<=n; ++j){

if(i>=j)

cout << setw(10)

<< c[(i-1)*i/2+j-1];

else

cout << setw(10) << 0.0;

cout << endl;

break;

case 2: cout.setf(ios::scientific|

ios::showpos);

cout << setprecision(1);

for(int i=1; i<=n; ++i){

for(int j=1; j<=i; ++j){

cout << setw(10)

161
<< c[(i-1)*i/2+j-1];

cout << endl;

break;

default:;

cout << "Folytatja? (I/N): "; cin >> ch;

}while(ch != 'n' && ch != 'N');

return 0;

162
C++ kislexikon

standard cin >> változó;


beolvasás
cin >> változó1 >> változó2;

Standard kiírás cout << kifejezés;

cout << kifejezés1 << kifejezés2;

természetes int n;
szám ellenőrzött
cin >> n;
beolvasása
if(cin.fail() || n<0){

cout << "Hibás szám!\n";

exit(1);

int n;

bool error;

do{

cin >> n;

error = cin.fail() || n<0;

if(error){

cout << "Hibás szám!\n";

cin.clear();

163
string tmp; getline(cin,str);

}while(error);

végtelenített char ch;


futtatás
do{

cout << "Folytatja? (I/N): ";

cin >> ch;

}while(ch != 'n' && ch != 'N');

menü char n;

do{

cout << "Menü pontok jelentése …";

cout << "Válassz: ";cin >> n;

switch(n){

case 1: … ; break;

case 2: … ; break;

default: … ;

}while(n != 0);

szerkesztett
input-output
#include <iomanip>

164
cin.setf(ios::flag)

cin.unsetf(ios:: flag)

cout.setf(ios:: flag)

cout.unsetf(ios:: flag1|ios:: flag2)

cout << manipulator

formátum jelzők scientific, fixed lebegőpontos ill.


fixpontos alak
(flag)
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 setw(int w) mezőszélesség


width(int w)
megadása
(manipulator)
setprecision(int p) számábrázolás
precision(int p)
pontossága

165
setfill(char c) kitöltő karakter
definiálása

endl sorvége

166
5. Szöveges állományok

A szöveges állományok (szöveges fájlok) az adatokat karakteres formában


háttértárolókon tárolják. Tartalma egy szöveg, amely lényegében egyetlen,
többnyire igen hosszú, a felhasználó által könnyen elolvasható karakterlánc.
A karakterek között lehetnek olyanok is, amelyek a szöveg megjelenítését
befolyásolják. Már egyszerű szövegszerkesztők is képesek egy ilyen szöveget
úgy megjeleníteni, hogy a szöveg ilyen speciális karaktereit a megjelenítés
formáját meghatározó jeleknek (tabulátor, sorvége) tekintik.
A szöveges állományokat kétféle célból fogjuk használni: olvasásra
illetve írásra. Ehhez az első esetben egy „vedd a következő adatot” jellegű
olvasó műveletre, a második esetben „tedd az eddigi adatok után” írás
műveletre lesz szükségünk.

Implementációs stratégia

Egy alkalmazás implementálásakor sokszor feltett kérdés az, hogy a bemenő


adatokat billentyűzetről vagy szöveges állományból vegyük-e, illetve az
eredményt terminálra vagy szöveges állományba írjuk-e. Egyszerűbb,
kevesebb előkészítést igényel a konzolablakos input-output, ráadásul egy
hibásan beírt adatot azonnal, a program futása közben lehet korrigálni, ha az
alkalmazás ezt lehetővé teszi. Kevés számú bemenő adat esetén ezt a
megoldást ajánljuk. Ha viszont sok bemenő adatra van szükség (például egy
mátrix elemeit kell megadni), akkor érdemes azokat egy szöveges
állományba előre beírni. Az alkalmazás tesztelésekor ez mindenképpen
kifizetődő, de a felhasználó számára is áttekinthetőbb, ha az adatokat
előzetesen egy szövegszerkesztőben láthatja. Sajnos hibás adat esetén a
pánikszerű leállás az egyetlen biztos módszer, hiszen futás közben már nem
lehet a szöveges állományon módosítani. Az eredmény szöveges állományba
történő írásának az a kézzel fogható előnye, hogy az a program befejeződése
után is olvasható lesz az eredmény.
Egy szöveges állomány szabadon szerkeszthető, ami különösen nagy
felelősséget ró az őt használó programra. A „bolond biztos” működés
biztosítása általában igen nehéz, ezért formai megkötéseket szoktak

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

168
állomány végét is egy speciális karakter jelzi, de a beolvasást végző nyelvi
elemek gyakran elfedik ezt, és más módon adják tudtunkra azt, hogy elértünk
az állomány végére.)
Hatékonyabb az a megoldás, amelyik egy fokozatosan nyújtózkodó
tömböt alkalmaz. Ennek mérete az állományból történő (while ciklusos)
olvasás során lépésről-lépésre nő, így a beolvasás végén éppen a kívánt
méretű tömbbel fogunk rendelkezni. Az azonban külön vizsgálandó, hogy a
választott programozási nyelv rendelkezik-e ilyen lehetőséggel (mint például
a C++ nyelv vector<> típusa vagy a C# List típusa), vagy ha nem, megéri-e
megteremteni a lehetőségét egy ilyen tömbnek.

Ha az állomány a tömb elemei előtt tartalmazza a tömb


méretét is
1. A szóba jöhető tömbméreteknek ismert a felső korlátja,
akkor egy ilyen maximális méretű tömböt definiálunk
fordítási időben, ennek elejére egy for ciklussal olvassuk
be az elemeket.
2. futási időben hozzuk létre a megadott méretű tömböt,
majd ebbe olvassuk be egy for ciklussal az elemeket.
Ha az állomány csak a tömb elemeit tartalmazza, a méretét
nem
3. A szóba jöhető tömbök méretének ismert a felső korlátja,
akkor egy ilyen maximális méretű tömböt definiálunk
fordítási időben, egy while ciklussal olvassuk be ennek
elejére az elemeket.
4. Futás közben nyújtózkodó méretű tömböt használunk, az
elemek olvasása egy while ciklussal történik.

5-1. ábra. Tömb állományból való feltöltésének módjai

Megemlítjük, de hatékonysági szempontok miatt egyáltalán nem


javasoljuk, azt a lehetőséget, hogy kétszer egymás után nyissuk meg
olvasásra a szöveges állományt. Először csak azért, hogy megszámoljuk hány

169
érték van benne (ehhez az előbb javasolt while ciklus kell), ezt követően
létrehozzuk a szükséges méretű tömböt, és egy második menetben
megfelelő számú értéket kiolvasva az állományból feltöltjük a tömböt (a már
korábban említett for ciklussal).
Az adott jelig vagy az állomány végéig tartó while ciklussal végrehajtott
olvasást többnyire előreolvasási technikával valósítjuk meg. Ez a
megállapítás a legtöbb nyelvre, köztük a C-szerű nyelvekre (C++-ra is) is igaz.
Ennek a feldolgozásnak az a lényege, hogy először megkíséreljük a soron
következő érték beolvasását, majd csak ezt követően vizsgáljuk meg, hogy
sikerült-e az olvasás (nem értünk-e az állomány végére, nem olvastunk-e
speciális jelet, amely az adatok végét jelzi), és csak ezután dolgozzuk fel a
beolvasott értéket. Egy ilyen feldolgozásban az olvasó utasítás a ciklusfeltétel
ellenőrzése előtt kell, hogy megjelenjen. Az alábbi algoritmus-séma mutatja
be az előreolvasási technikát.
következő érték olvasása

while( nincs fájlvége vagy

a beolvasott érték nem speciális jel){

beolvasott érték feldolgozása

következő érték olvasása

Ebben az olvasó utasítás két helyen is szerepel: a ciklus előtt és a ciklus


mag végén, mert ez biztosítja, hogy a ciklusfeltétel kiértékelése közvetlenül
az olvasás után történjen. A bemutatott technikának az előnye az, hogy a
beolvasandó adatok végét ugyanúgy kell vizsgálni akkor is, ha azt az állomány
vége jelzi, vagy ha egy speciális jel. (A C++ nyelv lehetőséget ad arra, hogy a
ciklusfeltétel helyén szerepeljen az olvasó utasítás, azaz formailag csak
egyszer.)

Nyelvi elemek

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

171
ofstream ofile

definiál egy úgynevezett bementi illetve egy kimeneti adatfolyam


objektumot (ezeknek a neve itt ifile és ofile). A fájlhasználathoz
szükséges nyelvi elemeket az fstream könyvtár definiálja, ezért a
programunk elején szükség lesz az #include <fstream> sorra.
Az adatfolyamokhoz tartozó fájlok neveit az
ifile.open(fnev.c_str())

ofile.open(fnev.c_str())

utasításokkal adhatjuk meg. Ezek a bemeneti adatfolyamhoz tartozó fájt meg


is nyitják, a kimeneti adatfolyamhoz tartozót pedig létre is hozzák.
Az adatfolyamhoz tartozó fájl nevét archaikus (C stílusú) karakterlánc
formában kell megadnunk. Ehhez elég annyit tudni, hogy egy string típusú
változóban (legyen a neve: str) tárolt (korszerű, C++ stílusú) sztringnek az
str.c_str() adja meg az archaikus alakját.
Az adatfolyam definíciója és a hozzátartozó fájl megnyitása
összevonható egy lépésbe.
ifstream ifile(fnev.c_str())

ofstream ofile(fnev.c_str())

A fájlok lezárására automatikusan sor kerül akkor, amikor az


adatfolyam élettartama lejár (például a vezérlés az adatfolyam deklarációját
tartalmazó blokk végéhez ér). A lezárás azonban explicit módon is
kikényszeríthető:
ifile.close()

ofile.close()

172
Egy fájl megnyitásakor különféle hibák történhetnek. A leggyakoribb
az, hogy a megnevezett állományt nem találjuk meg, mert vagy elfelejtettük
létrehozni, vagy nem abban a könyvtárban van, ahol keressük. Az esetleges
hibára a fail() függvénnyel kérdezhetünk rá.
ifstream ifile;

if(ifile.fail()){

cout << "Hiányzik az állomány\n";

return 1;

Ciklusba is szervezhetjük a fájlnyitást, csak ilyenkor a fájlt azonosító


adatfolyamot a clear() művelettel újra inicializálni kell, mielőtt újra
megpróbáljuk megnyitni. Erre példát a feladatok megoldó kódjaiban
mutatunk majd.
Az olvasás az ifile >> változó, az írás az ofile << kifejezés
utasítással történik, amely igencsak hasonlít a konzolos input-outputhoz.
Akárcsak ott, most is szöveges adatfolyam objektumokkal van dolgunk,
amelyekkel a szöveges állomány tartalmát, egy karakterláncot tudunk
megfelelően szeletelve beolvasni, vagy a kiírásnál összeállítani.

Fájlvége kezelése a programozási nyelvekben

Amikor egy szöveges állományból olvasunk egy C++ nyelv nyújtotta


adatfolyam segítségével, és az állomány karaktereit már sorban kiolvastuk,
akkor – tehát egy sikertelen olvasás után – beállítódik egy speciális jelzőbit
(eof flag), amely ezután azt mutatja, hogy az olvasás során az állomány
végéhez értünk. Fontos tudni, hogy az állomány megnyitása nem inicializálja
az eof flag értékét, azt csak az első olvasás teszi meg: sikeres olvasás esetén
0, sikertelen olvasás esetén 1. Az eof flag értékét közvetett módon az
olvasáshoz használt adatfolyam eof() függvényével kérdezhetjük le: igaz
érték ad vissza, ha a flag értéke 1, különben hamisat.

173
Ez a viselkedés az oka annak, hogy a C++ nyelven a már említett előre
olvasási technikát kell alkalmazni. Előbb kell olvasni, és utána kiértékelni az
olvasás eredményét. Az ismertebb programozási nyelvek ezt a megoldást
követik.
Ettől lényegesen eltér a Pascal programozási nyelv fájlkezelése. Ez a
nyelv egy olyan fájlolvasási műveletet ajánl fel, amely sikertelen olvasási
kísérlet esetén abortál. Ezért minden olvasás előtt meg kell vizsgálni, hogy
elértük-e a fájl végét. Ehhez a fájlkezelés egy speciális mutatót használ, amely
a szöveges állomány soron következő, még ki nem olvasott karakterére
mutat. Ha ez a karakter a sorvége jel, akkor elértük a fájl végét. Ezt az
eseményt itt is egy eof() függvénnyel kérdezhetjük le. De míg más
nyelvekben az eof() függvény akkor ad vissza igaz értéket, amikor már
kiolvastuk a fájlvége jelet, addig itt akkor, amikor elértük azt, de még nem
olvastuk ki.

A fájlból történő olvasás esetén a szöveges állomány aktuális, a már


feldolgozott karakterek utáni első karakterétől kezdve próbál beolvasni egy a
változó típusának megfelelő értéket. Alapértelmezett beállítás mellett átlépi
az elválasztó jeleket (szóköz, tabulátor jel, sorvége jel), majd sorban olvassa
azokat a karaktereket, amelyekből a változó számára érték állítható elő.
Például, ha a változó egész típusú, akkor az első ilyen karakter lehet számjegy
vagy egy előjel (+/-), az azt követő karakterek pedig számjegyek. Az olvasás
addig tart, amíg nem következik egy nem-számjegy karakter. Legjobb, ha az
értéket hordozó karaktersorozatot az állományban egy elválasztójellel zárjuk.
A következő olvasás majd ennél a karakternél folytatódik.
Hibás működést okoz, ha az olvasás nem tud megfelelő értéket
előállítani, például ha egy ’a’ betűt talál egy egész típusú változó
beolvasásához. Egy utasítással egyszerre, pontosabban egymás után több
változónak is lehet értéket adni: ifile>>változó1>> változó2.
C++ nyelven az olvasás nem okoz hibás működést, ha már elértük a fájl
végét. Ilyenkor egyetlen dolog történik: ettől kezdve az ifile.eof()
függvény igaz értéket ad vissza. Ügyeljünk arra, hogy mindaddig, amíg nem
hajtottunk végre olvasást, addig az ifile.eof() függvény értéke nem
megbízható. Tehát mindig előbb olvassunk, és csak utána vizsgáljuk meg az

174
eof()-ot. Ezt biztosítja az implementációs stratégiák között említett előre
olvasási technika.
Megjegyezzük, hogy a fail() függvény általánosabb hatású, mint az
eof(), mert nemcsak fájl vége esetén, hanem egyéb olvasási hibák esetén is
igazat ad. „Bolond biztosabb” lesz az alkalmazásunk, ha a fájlvége figyelést a
fail() függvénnyel végezzük.
Ha a szöveges állomány összes karakterét egyenként kell beolvasni,
akkor vagy ki kell kapcsolni az elválasztójelek átlépését
#include <iomanip>

ifile.unsetf(ios::skipws);

char ch;

ifile >> ch;

vagy másik műveletet kell választani:


char ch;

ifile.get(ch);

Ez utóbbinak megvan a kiíró párja is: ofile.put(ch), amely


egyetlen karakter kiírásánál ugyanúgy működik, mint az ofile << ch
utasítás.
Ha a szöveges állomány összes karakterét egyenként kell beolvasni,
akkor vagy ki kell kapcsolni az elválasztójelek átlépését
#include <iomanip>

ifile.unsetf(ios::skipws);

char ch;

ifile >> ch;

vagy másik műveletet kell választani:

175
char ch;

ifile.get(ch);

Ez utóbbinak megvan a kiíró párja is: ofile.put(ch), amely


egyetlen karakter kiírásánál ugyanúgy működik, mint az ofile << ch
utasítás.
Sokszor van szükség arra, hogy teljes sorokat olvassunk be egyben,
majd a beolvasott sorból szedjük ki a számunkra hasznos információt. A
getline() függvénnyel tudunk egy teljes sort,
string sor;

getline(ifile, sor);

vagy egy megadott jelig (karakterig) tartó karakterláncot beolvasni egy


szöveges állományból. Ez az elválasztó jel lehet a sorvége jel is, ekkor az
utasítás hatása megegyezik a fentivel.
getline(ifile, sor, '\n' );

Nem tartozik szorosan a szöveges állományok témakörébe, de mivel az


adatfolyamokkal kapcsolatos nyelvi lehetőség, ezért itt említjük meg az
úgynevezett sztring-folyamok (stringstream) használatát. Ez lehetőséget
nyújt arra, hogy egy tetszőleges sztringet, ugyanúgy, mint a konzolos input
vagy a szöveges állomány beolvasása esetén, „felszeleteljünk” és a szeleteket
megfelelő más típusú értékekké alakítsuk, vagy fordítva, ahogy a kiírásnál,
különféle értékeket egyetlen karakterláncba fűzzünk össze. A 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

176
ciklusmag mindig egy olvasással kezdődik, másrészt az olvasás egy logikai
értéket ad vissza, amely hamis, ha az olvasás sikertelen.
string str = "Alma a fa alatt";

istringstream is;

is.str(str);

string tmp;

while(is >> tmp) { cout << tmp << endl;}

Természetesen eltérő típusú értékek is kiolvashatóak egy sztringből,


csak tudni kell, hogy azokat milyen sorrendben fűztük korábban össze.
string str = "218 Gipsz Jakab 1987 4.8";

istringstream is;

is.str(str);

string family, first, birth;

int id;

double result;

is >> id >> family >> first >> birth >> result;

Az output sztring-folyam segítségével tetszőleges sztring állítható


össze úgy, hogy közben a sztringbe fűzött adatelemek konverziójára is sor
kerül:
ostringstream os;

os << "A " << 3.2 << " egy valós szám ";

string str = os.str();

177
Ebben a fejezetben találkozni fogunk a struktúra nyelvi elemmel.
Ennek segítségével olyan összetett adattípust definiálhatunk, amelynek egy
értéke több komponensből áll. Egy ilyen összetett érték, más néven rekord,
komponenseire név szerint lehet hivatkozni.
Az alábbi példa egy hallgató adatait (azonosító és két osztályzat)
összefogó rekord típusát írja le:
struct Hallg {

string eha;

int jegy1, jegy2;

};

Ha h egy Hallg típusú változó, akkor a h-ban tárolt érték egy rekord,
amelynek például az azonosítójára a h.eha kifejezéssel hivatkozunk, ezt a
sztringet le lehet kérdezni, meg lehet változtatni.

178
11. Feladat: Szöveges állomány maximális eleme

Olvassunk be egy szöveges állományból egész számokat egy tömbbe, és


válasszuk ki a tömb elemei közül a legnagyobbat, továbbá mondjuk meg,
hogy ez hányadik a számok között! Az állomány első eleme a feltöltendő
tömb hosszát mutatja, amit aztán ennek megfelelő számú egész szám követ.
Feltesszük, hogy az állományban csak egész számokat helyeztek el elválasztó
jelekkel (szóköz, tabulátor, sorvége) határolva. Az eredményt írjuk ki a
szabványos kimenetre (képernyőre)!
A feladat csak a bemenő adatok megadási módjában tér el a 6.
feladatban megoldott problémától, így a megoldás terve megegyezik az ott
látottakkal.

Specifikáció

A = ( v : ℤn, max, ind : ℤ )


Ef = ( v=v n>0 )
n
Uf = ( v=v’ max = v[ind] = max v[i] ind [1..n]
i 1

Absztrakt program

A feladat visszavezethető a maximum kiválasztás tömbökre adaptált


programozási tételére:

max, ind := v[1], 1


i = 2 .. n
v[i]>max
max, ind := v[i], i SKIP

Implementálás

179
Csak a tömb szöveges állományból történő feltöltése résszel kell
foglalkoznunk, a kód többi része megegyezik a 6. feladatnál mutatott
kódrészekkel.
Először megkérdezzük annak a szöveges állománynak a nevét,
amelyben a bemenő adatokat tároljuk. Ezt a szabványos bementről olvassuk
be egy sztringbe.

string filename;

cout << "A fájl neve:"; cin >> filename;

Ezután definiálunk és megnyitunk egy bementi adatfolyamot, amely


segítségével a szöveges állomány adatait elérhetjük. Ennek az objektumnak a
neve legyen inp, amelyet ifstream típusúnak választunk. Ennek a típusnak
a használatához hivatkoznunk kell a program elején az fstream könyvtárra
(#include <fstream>). Az inp megnyitásakor (open) kell megadnunk a
szöveges állomány már bekért nevét archaikus (C-stílusú) karakterlánc
formában: filename.c_str().
Mivel a fájl megnyitásakor különféle hibák fordulhatnak elő (nincs
állomány, más néven szerepel, nincs jogunk létrehozni, stb.), felkészülünk
azok észlelésére, és a felhasználó tájékoztatása után megkíséreljük újra a
műveletet. Az alábbi kód az input szöveges állományt nyitja meg, ennek
sikerességét az inp.fail() logikai kifejezés vizsgálatával ellenőrizzük. A
clear művelet az ismételt fájlnyitáshoz kell.

ifstream inp;

do{

string filename;

cout << "\nKérem a fájl nevét: " ;

cin >> filename;

inp.clear();

180
inp.open(filenev.c_str());

if(inp.fail())

cout << "A megadott fájlt nem találom! \n";

}while(inp.fail());

A sikeres megnyitás után beolvashatjuk az állományban tárolt


adatokat egy tömbbe. A tömb definíciója előtt meg kell ismerni a tömb
elemeinek számát, azaz a tömb hosszát. Ebben a feladatban ezt a szöveges
állomány tartalmazza, így először onnan a feldolgozásra váró elemek számát
olvassuk be. Az olvasás módja azonos a szabványos bementről történő
olvasással, azzal a különbséggel, hogy itt nem a cin, hanem az inp bemeneti
adatfolyamot használjuk.
int n;

inp >> n;

Meg kell győződnünk arról, hogy ez az olvasás sikerült-e, azaz


inp.fail() hamis-e, valamint azt, hogy teljesül-e a darabszámra a feladat
előfeltétele: ez előírja, hogy a tömb hossza egy pozitív egész szám.

int n;

inp >> n;

if (inp.fail() || n<1){

cout << "Az állomány tartalma nem megfelelő!\n";

return 1;

Megjegyezzük, hogy itt szó sem lehet arról, hogy valamilyen do-while
ciklussal végezzük az adatellenőrzést. A szöveges állomány ugyanis már

181
készen van, olvasásra megnyitottuk, ezért futás közben nincs lehetőség a
tartalmának megváltoztatására. Hibás adat esetén le kell állítani a
programot, hogy a szöveges állományt kijavíthassuk.
Megfelelő darabszám ismeretében létrehozhatjuk azt a tömböt,
amelyben a bemenő értékeket helyezzük el. Ha feltételezhetjük, hogy az
állomány elején megadott darabszám helyes, azaz a darabszámot annyi szám
követi az állományban, amennyi a darabszám értéke, akkor az alábbi kóddal
feltölthetjük a tömböt.
vector<int> v(n);

for(int i=0; i<n; ++i) inp >> v[i];

Ha a darabszám kisebb a szöveges állományban ténylegesen


elhelyezett számok számánál, akkor nem fog minden szám bekerülni a
tömbbe; ha nagyobb, akkor a tömb végén lesznek definiálatlan értékek is. Az
utóbbi esetben a maximum kiválasztás hibás eredményt is okozhat. Éppen
ezért biztonságosabb az alábbi kód, amelyik leállítja az olvasást, ha az végére
ért az állománynak, és figyeli azt is, hogy az egyes számok beolvasása
rendben történt-e.

vector<int> v(n);

for(int i=0; i<n; ++i){

inp >> v[i];

if(inp.eof()) break;

if(inp.fail()){

cout << "Az állomány tartalma nem jó!\n";

return 1;

182
A beolvasás után a tömb elemeit kiírjuk a szabványos kimenetre.

cout << "A tömb hossza: " << n << endl;

cout << "A tömb elemei: ";

for (int i=0; i<n-1; ++i) cout << v[i] << ", ";

cout << v[n-1] << endl;

Tesztelés

A teszteléshez a 6. feladat teszteseteit használhatjuk. Ezeken kívül a


beolvasás számára kell fehér doboz teszteseteket megfogalmazni.
1. Nem létező állomány megadása illetve ismételten nem létező
állomány megadása.
2. Üres állomány.
3. Első adat nem természetes szám.
4. Első szám nem pozitív.
5. Az első szám nem az azt követő elemek darabszáma (nagyobb ill.
kisebb).
6. Az állományban számok helyett szövegek vannak.
7. Az állományban „túl nagy” szám is van.

183
Teljes program

Tekintsük meg végül a teljes programkódot!


#include <iostream>

#include <fstream>

#include <vector>

#include <string>

using namespace std;

int main()

// Az állomány nevének bekérése

string filename;

ifstream inp;

do{

cout << "\nKérem a fájl nevét: " ;

cin >> filename;

inp.clear();

inp.open(filename.c_str());

if(inp.fail())

cout << "A megadott fájlt nem találom!\n";

}while(inp.fail());

// Beolvassuk és ellenőrizzük a tömb hosszát

184
int n;

inp >> n;

if (inp.fail() || n<1){

cout << "Az állomány tartalma nem jó!\n";

return 1;

// Létrehozunk egy tömböt, feltöltjük, majd kiírjuk

vector<int> v(n);

for(int i=0; i<n; ++i){

inp >> v[i];

if(inp.eof()) break;

if(inp.fail()){

cout << "Az állomány tartalma nem jó!\n";

return 1;

cout << "A tömb hossza: " << n << endl;

cout << "A tömb elemei: ";

for (int i=0; i<n-1; ++i) cout << v[i] << ", ";

cout << v[n-1] << endl;

185
// Maximum kiválasztás

int ind = 0, max = v[0];

for(int i=1;i<n;++i){

if (v[i]>max){ ind = i; max = v[i]; }

// Kiíratás

cout << "A tömb egyik legnagyobb eleme: "

<< max << endl;

cout << "Ez a " << (ind+1) << ". elem." << endl;

return 0;

186
12. Feladat: Jó tanulók kiválogatása

Válogassuk ki egy tömbben tárolt hallgatói adatok közül a jó tanulókat! Jó


tanuló az, akinek nincs négyesnél rosszabb jegye. Ehhez ismerjük a hallgatók
kódját és két osztályzatát. Gyűjtsük ki azon hallgatók kódjait, akiknek
mindkét osztályzata legalább négyes! A tömböt egy szöveges állományból
töltsük fel. A szöveges állományban soronként helyezkednek el a hallgatók
adatai. Egy sorban az első hét karakter a kód, utána egy szóköz, azután az
első osztályzat (számjegy), majd egy szóköz, és a második osztályzat.

Specifikáció

A = ( adat : Hallgn, ki : String* )


Hallg=rec(eha:String, jegy1,jegy2:ℕ)
Ef = ( v=v’ )
n
Uf = ( v=v’ ki = adat[i].eha )
i 1
adat[i ]. jegy1 3
adat[i ]. jegy2 3

Absztrakt program

A feladat visszavezethető egy olyan összegzésre, ahol a hozzáadás művelete


helyett a hozzáfűzés műveletét használjuk, de csak az utófeltételben leírt
feltétel teljesülése esetén:

ki := <>
i = 1 .. n

adat[i].jegy1>3 adat[i].jegy2>3

ki := ki <adat[i].eha> SKIP

Implementálás

187
A programkód két fő részből áll: először feltöltjük az adat tömböt a szöveges
állománybeli adatokkal, utána pedig elvégezzük a kiválogatást. Külön kiírás
részre nincs szükség, ha az implementációban a ki kimeneti-változót a cout
kimeneti adatfolyammal helyettesítjük. Erre az ad lehetőséget, hogy a
kimeneti-változó egy kód sorozatot tárol és egy kód egy sztring, tehát
összességében az eredmény egy karakterfolyam, amelyet közvetlenül a cout
kimeneti adatcsatornára küldhetünk.
Először azonban definiáljuk a hallgatói adatokat tartalmazó struktúrát.

struct Hallg {

string eha;

int jegy1, jegy2;

};

A hallgató a feladat szempontjából egy összetett, több részből álló


adat: a hallgató kódja és két osztályzata. Ezt a fenti kóddal írhatjuk le. A
Hallg egy új típus, amelynek értékei a fenti három adattal rendelkező
hallgatók lehetnek. Segítségével definiálhatunk változókat
Hallg h

amelyek képesek egy kód és két osztályzat tárolására. Az egyes részekre


h.eha, h.jegy1, h.jegy2 formában lehet hivatkozni. A Hallg típust
használhatjuk egy olyan tömb deklarálására is, amelyik elemei hallgatók:
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

188
tárolva a szöveges állomány elején. Szerencsére egy vector<> típusú
tömbhöz hozzá is lehet fűzni elemeket.
vector<Hallg> adat;

Hallg h;

adat.push_back(h);

Egy hallgató adatainak a szöveges állományból való beolvasására két


megoldás is kínálkozik. Az egyikben azt használhatjuk ki, hogy a szöveges
állományban a szabványos elválasztó jelekkel (szóköz, sorvége jel) vannak az
adatelemek egymástól elhatárolva.
inp >> h.eha >> h.jegy1 >> h.jegy2;

A másik módszernél azt használjuk ki, hogy az összetartozó adatok


soronként helyezkednek el, és minden soron belül rögzített egy adatelem
kezdőpozíciója és mérete. A kód a 0. pozíción kezdődik és 7 karakter hosszú,
az első jegy a 8., a második jegy a 10. pozíción található. Először beolvassuk a
teljes sort egy sztringbe,
string sor;

getline(inp, sor);

majd ebből mazsolázzuk ki az adatelemeket. A substr() egy karakterlánc


megadott pozíciójú karakterétől kezdődően megadott hosszú rész-sztringjét
adja vissza.
h.eha = sor.substr(0,7);

Ha egy adatelem nem sztring, akkor azt át is kell alakítani megfelelő típusúra
(ezt az előző módszernél a beolvasó operátor elvégezte helyettünk)
h.jegy1 = atoi(sor.substr(8,1).c_str());

h.jegy2 = atoi(sor.substr(10,1).c_str());

189
Az atoi() helyett használható az alábbi megoldás is:
istringstream is; // #include <sstream>

is.str(sor.substr(8,1));

is >> h.jegy1;

is.clear();

is.str(sor.substr(10,1));

is >> h.jegy2;

A fenti módszerek mindegyike feltételezi, hogy a szöveges állomány


tényleg a megadott formában tárolja az adatokat.
Egyértelmű, hogy ennél a feladatnál a legelső olvasási módszer a jobb,
mert egyszerűbb. Nem működne viszont abban az esetben, ha a kódok
helyett nevek lennének a szöveges állományban, hiszen név tartalmaz
szóközt is, amely a beolvasás szempontjából elválasztó jel. (Ráadásul azt sem
tudjuk, hogy hány szóköz van egy névben, hiszen egy hallgatónak több
keresztneve is lehet.)
Az adatoknak a szöveges állományból való beolvasását végző ciklust
előre-olvasási technikával kell kódolni, azaz a ciklus előtt és a ciklus mag
végén is olvasni kell a fájlból. A ciklusfeltételben vizsgálhatjuk, hogy nem
értünk-e a fájl végére. (Erre az alábbiakban az eof() függvény helyett a
fail()-t használjuk.) Az előre-olvasást végző ciklus magjában mindig az egy
lépéssel korábban beolvasott adatokat dolgozzuk fel, azokat fűzzük hozzá a
tömbhöz.

vector<Hallg> adat;

Hallg h;

inp >> h.eha >> h.jegy1 >> h.jegy2;

while(!inp.fail()) {

190
adat.push_back(h);

inp >> h.eha >> h.jegy1 >> h.jegy2;

Absztrakt program

Az absztrakt program kódolásánál figyelembe kell venni, hogy nincs szükség a


ki-t helyettesítő cout kezdeti értékadására. A hozzáfűzés műveletét a kiíró
operátor (<<) helyettesíti. Ne felejtsük el a kiírt kódokat valahogyan
elválasztani egymástól, például mindegyik kerüljön új sorba.
Ügyeljünk arra is, hogy a C++ nyelvbeli tömb 0-tól indexeli az elemeit,
ezért a tervben szereplő 1..n intervallumot a 0..n-1 intervallum váltja fel.

cout << "A jó tanulók kódjai:\n";

for(int i=0; i<(int)adat.size(); ++i){

if(adat[i].jegy1>3 && adat[i].jegy2>3){

cout << adat[i].eha << endl;

Tesztelés

A teszteléshez a 6. feladata teszteseteit használhatjuk. Ezeken kívül a


beolvasás számára kell fehér doboz teszteseteket megfogalmazni.
1. Nem létező állomány megadása illetve ismételten nem létező
állomány megadása.
2. Üres állomány.
3. Első adat nem természetes szám.

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

192
Teljes program

#include <iostream>

#include <fstream>

#include <sstream>

#include <vector>

#include <string>

using namespace std;

struct Hallg{

string eha;

int jegy1, jegy2;

};

int main()

ifstream inp;

do{

string fajlnev;

cout << "\nKérem a fájl nevét: " ;

cin >> fajlnev;

inp.clear();

inp.open(fajlnev.c_str());

if(inp.fail()){

193
cout << "A megadott fájlt nem találom! \n";

}while(inp.fail());

vector<Hallg> adat;

Hallg h;

inp >> h.eha >> h.jegy1 >> h.jegy2;

while(!inp.fail()) {

adat.push_back(h);

inp >> h.eha >> h.jegy1 >> h.jegy2;

cout << "A jó tanulók kódjai:\n";

for(int i=0; i<(int)adat.size(); ++i){

if(adat[i].jegy1>3 && adat[i].jegy2>3){

cout << adat[i].eha << endl;

return 0;

C++ kislexikon

194
Szöveges állományok
kezelését támogató
#include <fstream>
csomag

Szöveges állományhoz
adatcsatorna definiálása
ifstream f; ofstream f;

Szöveges állomány ifstream f("fajlnev.txt");


megnyitása olvasásra
string filename = "fajlnev.txt";
(output fájl ugyanígy) ifstream f(filename.c_str());

ifstream f;

f.open("fajlnev.txt");

Szöveges állomány ifstream f(filename.c_str());


megnyitása ellenőrzéssel
if(f.fail()){

cout << "Nyitási hiba!\n ";

return 1;

ifstream f;

do{

string fnev;

cout << "\nFájl neve: ";

cin >> fnev;

f.clear();

f.open(fnev.c_str());

if(f.fail())

cout << "Nyitási hiba!\n";

195
}while(!f.fail())

f >> valtozo;

Olvasás szöveges
állományból char ch; //#include <iomanip>

f.get(ch); 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 f.close();


lezárása

Tömb feltöltése szöveges Element t[size];


állományból előre
for(int i=0; i<size; ++i){
megadott mérettel
f >> t[i];

196
Fokozatosan nyújtózkodó vector<Element> t;
tömb feltöltése szöveges
Element item;
állományból előre olvasási
technikával fájl végéig 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 string str = "Alma 2 fa -12.4";


sztring elemeinek típusos
istringstream is;
olvasása
is.str(str);

197
int n; string s; double f;

is >> word >> n >> word >> f;

198
II. RÉSZ
PROCEDURÁLIS PROGRAMOZÁS

Egy összetett feladat megoldását jól körülhatárolható részfeladatok


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

199
üzenetkommunikációt. Fontos, hogy az egyes részek közötti kapcsolat
egyszerű, jól átlátható legyen, minél inkább szolgálja az egyes részek
önállóságát, és szoros legyen az elkülönített részeken belüli összetartozás.
Már a korai programozási nyelvek is rendelkeztek olyan nyelvi
elemekkel, amelyekkel egy-egy részprogramot ki lehetett jelölni egy
programon belül. A mai nyelvekben már számos eszköz található a program
egyes részeinek leírására. Végső soron már ilyen az is, ha kommentekkel
elhatárolva különítünk el egy-egy kódrészletet vagy úgynevezett utasítás
blokkba zárjuk azt, de a procedurális programozás elsőszámú nyelvi elemei
kétségkívül az alprogramokat kódszinten kifejezni képes programfüggvények
és eljárások.
Az által, hogy a programunk hierarchiája a részekre bontás során
bonyolultabbá válik, még inkább előtérbe kerül annak az igénye, hogy 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.

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

201
6. Alprogramok a kódban

Programjainkban a funkcionálisan összetartozó utasításokat, azaz egy-egy


részfeladat megoldásáért felelős programrészeket külön egységekbe,
úgynevezett alprogramokba (procedúrákba) szervezhetjük. Egy alprogram
olyan önálló kódrész, amelyiknek a végrehajtását egy speciális utasítás, az
alprogram hívása kezdeményez. Ekkor átkerül a vezérlés az alprogramhoz,
majd annak befejeződésekor a hívó utasítás utáni pontra tér vissza.
Egy alprogram nem független az őt tartalmazó programtól, adatokat
kap tőle és adatokat ad neki vissza. Az adatáramlás többféle módon is
megvalósulhat az alprogram és környezete között. Ha az alprogramba
bekerülő és belőle kikerülő adatokat egyértelműen jellemezzük, akkor
paradox módon egy alprogramokra tagolt program sokkal erősebb kohéziót
valósít meg a program egyes részei között, mint egy monolit, alprogramokra
nem tördelt program, mert felügyeltté válik az egyes részek közötti
adatcsere.

Implementációs stratégia

Egy programkódban többféle szempont alapján lehet alprogramokat


kialakítani.

1. Önálló funkciót betöltő, sokszor már a tervezésnél felfedett jól


körülhatárolt résztevékenységet végző programrész elkülönített
kódolása.

2. Programban több helyen megismétlődő kódrész egy helyen való


közös leírása, amellyel csökkenthető a kód mérete és
biztonságosabb lesz a javítása.

3. Önmagában átlátható, áttekinthető kódrész összefogása.


6-1. ábra. Alprogram kialakításának okai

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

203
alprogramra nézve is globális, akkor azt mindkettő használhatja: olvashatja
és felülírhatja. A globális változók használata első látásra egy igen egyszerű
formája az adatcserének, de nagymértékben rontja a program
áttekinthetőségét. Egy alprogram működésének megértését ugyanis
akadályozza, ha minduntalan ki kell tekinteni az alprogramból, és megnézni,
hogy egy alprogramban használt globális változó a program melyik részén
keletkezett, mikor, milyen értéket kapott, hol lesz még felhasználva, stb.
Ezért csak nagyon indokolt esetben engedélyezzük a globális változók
használatát. Ilyen lehet például az, ha egy adatot az összes alprogram
használja, és megjegyzésként pontosan megjelöljük, hogy melyik alprogram
olvassa ezt az adatot, melyik az, amelyik meg is változtatja, továbbá erősen
korlátozzuk az adat értékét megváltoztató alprogramok számát.

Globális változó: Már az alprogramon hívása előtt létrehozott olyan


változó, amelyet az alprogram is használhat.

Lokális változó: Kizárólag az alprogramban használt változó.

Bemenő paraméterváltozó: Az alprogram olyan lokális változója, amely


az alprogram hívásakor a környezetétől (annak egy változójától vagy
kifejezésétől) kezdő értéket kap.

Eredmény paraméterváltozó: Az alprogram olyan lokális változója,


amelynek az alprogram befejeződésekor az értéke visszakerül a környezet
egy változójába.
6-2. ábra. Alprogram változói implementációs szempontból

Az alprogrammal való kapcsolattartásnak sokkal biztonságosabb


megoldása az, ha az alprogram kizárólag a lokális változóit használja, és ezek
közül jelöli ki azokat, amelyeken keresztül adatcserét hajthat végre a hívó
programrésszel. Ezek az úgynevezett paraméterváltozók. Az alprogram
formális paraméterlistája sorolja fel az alprogram paraméterváltozóit, rögzíti
azok típusát és sorrendjét. Külön meg kell jelölni, hogy mely
paraméterváltozók bonyolítanak bemenő adatforgalmat, és melyek kimenőt.
A bemenő paraméterváltozók azok, amelyek a hívás során a hívóprogram

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

Nyelvi elemek

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

206
A C++ nyelv kétféle paraméter átadási módot ismer. Érték szerinti
paraméterátadással a bemenő adatot tudjuk a bemenő
paraméterváltozóhoz eljuttatni, a hivatkozás (referencia) szerinti
paraméterátadás mindkét irányú adatáramlást támogatja.
Érték szerinti paraméterátadás valósul meg az alábbi kódban. Itt az x
változó egy bemenő paraméterváltozó. Híváskor az v változó értéke
átmásolódik az x változóba, amely egy önálló memória területtel rendelkező
lokális változója a függvénynek. A hívás után már semmi kapcsolat nincs a v
és az x változók között.
hívás: int v = 23; fv(v);
hívott: void fv(int x) { }

Paraméterátadási módok

Érték szerinti paraméterátadás


Az aktuális paraméter értéke az alprogram hívásakor átmásolódik a neki
megfeleltetett paraméterváltozóba, és annak kezdőértékévé válik. A
paraméterváltozó ettől eltekintve a hívó programrésztől teljesen függetlenül,
az alprogram lokális változójaként használható.
Cím vagy hivatkozás (referencia) szerinti paraméterátadás
Az alprogram paraméterváltozója nem rendelkezik olyan önálló
memóriafoglalással, ahová az aktuális paraméter értékét át lehetne másolni.
A paraméterváltozó vagy egy pointer, amelyik a memóriafoglalás címét kapja
értékül (cím szerinti paraméterátadás), vagy egy úgynevezett
referenciaváltozó, amelyik az aktuális paraméterként megadott változó
memóriafoglalását használja, mintha annak a változónak egy másodlagos
(alias) neve lenne (hivatkozás szerinti paraméterátadás).
Érték-eredmény szerinti paraméterátadás
Az érték szerinti paraméterátadást kombináljuk azzal, hogy amikor az
alprogram befejeződik, akkor az aktuális paraméterként megadott változóba
átmásolódik a megfeleltetett paraméterváltozó értéke. Ettől eltekintve a
paraméterváltozó teljesen különálló változóként, az alprogram lokális
változójaként használható.

207
Név szerinti paraméterátadás
A paraméterváltozó egy sablon, amely helyébe egy-egy konkrét hívás
ismeretében szövegszerűen másolódik be a megfeleltetett aktuális
paraméter.

Hivatkozás szerinti paraméterátadás történik az alábbi példában. Az x


paraméterváltozó nem egy önálló új változó, hanem egy úgynevezett
referenciaváltozó, amelyik azonos memória területet használ a hívás helyén
feltüntetett a v változóval. Bármit változtat az alprogram az x változóban az
a változás v változóban is megjelenik. Az x változó valójában egy bemenő- és
eredmény paraméterváltozó egyben, hiszen az előbb leírtak szerint már a
létrejöttekor (híváskor) látható x változóban a v értéke, és a hívó programba
történő visszatérés után a v változóban a x értéke.
hívás: int v = 23; fv(v);
hívott: void fv(int &x) { }

Általános szabályként alkalmazzuk a C++ nyelvben azt, hogy a kizárólag


bemenő paraméterváltozókhoz az érték szerinti paraméterátadást, az
eredmény paraméterváltozókhoz pedig a hivatkozás szerinti
paraméterátadást használjuk. Hatékonysági okból azonban célszerű az
összetett típusú bemenő paraméterváltozóknál is a hivatkozás szerinti
paraméterátadást alkalmazni. Az érték szerinti paraméterátadás esetén
ugyanis mindig lemásolódik az átadott paraméter értéke, hiszen ilyenkor a
paraméterváltozó önálló memóriafoglalással rendelkezik. A memóriafoglalás
ilyen duplázása nem okoz gondot egy egyszerű (tehát néhány bájtot
elfoglaló) adat esetén, de összetett típusú adatoknál ez már nagy veszteséget
jelenthet. Ezt elkerülendő, ilyenkor is hivatkozás szerinti paraméterátadást
alkalmazunk. Ennek a megoldásnak a szépséghibája az, hogy egy ilyen
paraméterváltozó egyben eredmény paraméterváltozó is lesz, tehát képes a
neki átadott paramétert megváltoztatni. Hogy ez mégse történjen meg (nem
elégszünk meg a programozó „isten bizony nem változtatom meg”
ígéretével) a const kulcsszót írjuk a paraméterváltozó típusa elé. Ennek
eredményeként fordítási hibát fogunk kapni, ha az alprogramban meg
akarnánk változtatni az ilyen paraméterváltozó értékét. Összefoglalva tehát,

208
az összetett típusú kizárólag bemenő paraméterváltozókat konstans
referenciaváltozóként deklaráljuk. (Külön szabály vonatkozik az eredeti C++-
os tömb típusú paraméterekre, de mivel helyettük a vector típust
használjuk, ezért erre itt nem térünk ki.)
A C++ nyelvben minden alprogramot függvénynek nevezünk. A
függvény fejében legelőször a visszatérési érték típusát kell megadni, majd
azt követően a függvény nevét, azután a paraméterlistáját. A függvénynek
egyetlen, de tetszőleges típusú visszatérési értéke lehet. Ha több értéket kell
visszaadni, akkor azokat összefoghatjuk egy tömbbe vagy egy rekordba (egy
struktúrába), és így formailag egyetlen értéket adunk vissza. A függvény
törzse kapcsos zárójelek közé kerül. A visszatérési értéket a függvény
kódjában egy kitüntetett utasításban (úgynevezett return utasításban)
elhelyezett kifejezés formájában kell leírni. Ennek az utasításnak a
végrehajtása a függvény befejeződését vonja maga után. Az eljárásokat úgy
jelöljük, hogy a visszatérési típus helyére a void kulcsszót írjuk. Ilyenkor nem
kell return utasítást használni, de üres return utasítással kikényszeríthetjük az
eljárás befejeződését. Tehát C++ nyelven az eljárás egy visszatérési érték
nélküli függvény.

Mi történik egy alprogram hívásakor?


Amikor egy alprogram hívására sor kerül, annak érdekében, hogy majd a
meghívott alprogram befejeződésekor vissza lehessen térni a hívó
programrészhez, számos, a hívó programrész működésével kapcsolatos
információ elmentésre kerül. A hívó programrész lokális változói már a hívás
előtt a verem memóriában (STACK) vannak. A híváskor ide másolódnak a
központi egység regisztereinek értékei is, mint például az utasítás számláló
(PC), utasítás regiszter (IR), akkumulátor (AC) stb. Csak ezeket követően
kerülnek a verem memóriába a hívott alprogrammal kapcsolatos adatok,
például ekkor foglalnak itt automatikusan helyet a meghívott alprogram
lokális változói, köztük a paraméterváltozói is, és a bemenő
paraméterváltozók megkapják kezdőértéküket
A return utasítás hatására a verem memóriából törlődnek a hívott
alprogrammal kapcsolatos adatok miután a kimenő paraméterváltozók

209
érékei és az esetleges visszatérési érték a hívó program megfelelő helyére
került, a verem memóriából visszatöltődnek a korábban elmentett értékek a
regiszterekbe, hogy a hívó programrész folytatódhasson.

A bemutatott programjainkban már eddig is használtunk függvényt,


hiszen a main() egy olyan „alprogram”, amelyet az operációs rendszer hív
meg, befejeződésekor oda tér vissza a vezérlés. A main függvénynek lehet
bemenő adatokat is adni, amelyeket például egy parancssorból történő
futtatás kezdeményezésekor adhatunk meg, és a visszatérési értékét is le
lehet kezelni.

210
13. Feladat: Faktoriális

Számoljuk ki egy adott természetes szám faktoriálisát!

Specifikáció

A feladat specifikációjában a faktoriális számításnak az eredeti definícióját


adjuk meg, nevezetesen, hogy n! = 2 * 3 * … * n. A 0 és az 1 faktoriálisa
definíció szerint 1.

A = ( n : ℕ, 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.

211
A program kerete

A main függvény beolvas a billentyűzetről egy természetes számot,


kiszámolja a faktoriálisát, majd kiírja az eredményt a konzol ablakba.
int n = ReadNat("Kérem a számot: ",

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

212
cout << "Faktoriális = " << Factorial(

ReadNat("Kérem a számot: ",

"Természetes számot kérek!" ));

cout << "\n\nFolytatja? (I/N)"; cin >> ch;

getline(cin,tmp);

}while(ch != 'n' && ch != 'N');

return 0;

Az alprogramok közötti hívási kapcsolatot teszi szemléletessé az alábbi


ábra. A nyilak jelzik, hogy melyik alprogram hívja a másikat. A nyilak mellé
oda lehetne még írni az adatforgalmat is: milyen bemenő adat adódik át
híváskor, és milyen eredményt szolgáltat a meghívott alprogram.

ReadNat()

main()

Factorial()

6-3. ábra. Alprogramok hívási láncai

Mindhárom alprogram ugyanabban a forrásállományban (legyen


ennek a neve main.cpp) foglal majd helyet. Ebben elől az iostream
csomag bemásolása és a using namspace std utasítás áll, ezután a main
függvényből meghívott függvények deklarációi, majd a main függvény, és
végül a másik két függvény definíciója.

213
Absztrakt program kódolása

Az absztrakt program alapján a Factorial() függvény törzsét készíthetjük


el, annak kódja a struktogramm szó szerinti lefordításának eredménye.

int Factorial(int n)

int f = 1;

for(int i = 2; i<=n; ++i){

f = f * i;

return f;

Az absztrakt program három változót használ. Az n változó a


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

214
Beolvasás

A beolvasáshoz használt függvény két paraméterváltozóval


rendelkezik. Mindkettőt kizárólag bemenő paraméterváltozóként fogjuk
használni, de mivel összetett (sztring) típusúak, ezért nem érték szerinti,
hanem konstans hivatkozás szerinti paraméterátadással oldjuk meg a kezdeti
értékadásukat.

int ReadNat(const string &msg, const string &errmsg)

int n;

bool error = true;

do{

cout << msg; cin >> n;

if(error = cin.fail() || n<0){

cout << errmsg << endl;

cin.clear();

string tmp; getline(cin,tmp);

}while(error);

return n;

A függvény törzsében a korábban már ismertetett technikával


próbálunk egy billentyűzetről egy nem negatív számot beolvasni. Ha a
megadott adat nem szám vagy negatív, hibajelzést adunk, és újra
megkíséreljük a beolvasást. Ennél fogva ennek a függvénynek csak akkor
fejeződik be a végrehajtása, ha sikerül egy természetes számot beolvasni.

215
Tesztelés

A teszteseteket alprogramonként, azaz modulonként adjuk meg.


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

216
Teljes program

#include <iostream>

#include <string>

using namespace std;

int ReadNat(const string &msg,

const string &errmsg);

int Factorial(int n);

int main()

cout << "Egy szám faktoriálisát számolom?\n";

string tmp;

char ch = 'i';

do{

cout << "Faktoriális = " << Factorial(

ReadNat("Kérem a számot: ",

"Természetes számot kérek!" ));

cout << "\n\nFolytatja? (I/N)"; cin >> ch;

getline(cin,tmp);

}while(ch != 'n' && ch != 'N');

return 0;

int Factorial(int n)

217
{

int f = 1;

for(int i = 2; i<=n; ++i){

f = f * i;

return f;

int ReadNat(const string &msg, const string &errmsg)

int n;

bool error = true;

string tmp;

do{

cout << msg; cin >> n;

if(error = cin.fail() || n<0){

cout << errmsg << endl;

cin.clear();

string tmp; getline(cin,tmp);

}while(error);

return n;

14. Feladat: Adott számmal osztható számok

218
Válogassuk ki egy egész számokat tartalmazó tömb adott k számmal
osztható elemeit egy sorozatba!

Specifikáció

A feladat egy egész számokból álló sorozatot kíván összefűzni a tömb k-val
osztható elemeiből.

n *
A = ( t : ℤ , 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.

219
A tömböt vector<int> típussal fogjuk ábrázolni, amely 0-tól
kezdődően indexelődik, ezért az absztrakt program ciklusváltozója a 0..n-1
intervallumot fogja befutni.
Az eredmény sorozatot a cout adatfolyammal valósítjuk meg, így a <<
operátor helyettesíti a specifikációban használt operátort. Ennek
következményeként az eredmény sorozathoz hozzáfűzött elemek közvetlenül
a konzolablakban jelennek majd meg.
Bevezetünk három függvényt. Az egyikben az állomány megnyitására
és a tömb feltöltésére (FillVector) kerül sor, a másikban egy nem-nulla
egész szám beolvasására (ReadNotNullInt), a harmadikban (Selecting)
az absztrakt programot kódoljuk.

A program kerete

Egy forrásállományban (legyen ennek a neve main.cpp) helyezzük el a main


függvényt, amelyet megelőz az iostream, az fstream és a vector csomag
bemásolása, a using namespace std utasítás, majd a függvények
deklarációi, amelyek definíciója a main függvény után helyezkedik el.

#include <iostream>

#include <fstream>

#include <vector>

#include <string>

using namespace std;

vector<int> FillVector();

int ReadNotNullInt(const string &msg,

const string &errmsg);

void Selecting(const vector<int> &t, int k);

220
int main()

cout << "Adott számmal osztható számok\n";

vector<int> t = FillVector();

int k = ReadNotNullInt("\nKérem az osztót: ",

"Egész számot kérek!\n");

cout << "Eredmény: ";

Selecting(t,k);

return 0;

A main függvény kódjából jól látható, hogy a program mely


függvényeket hívja meg.

FillVector()

main() ReadNotNullInt()

Selecting()

6-4. ábra. Alprogramok hívási láncai

A main egy tömböt kap a FillVector() függvénytől, egy egész


számot a ReadNotNullInt()-től. A Selecting() megkapja a tömböt, a
nullától különböző számot, és kiírja a szabványos kimenetre a tömb adott
számmal osztható elemeit.

Tömb beolvasása

221
A FillVector() függvény egy vector<int> típusú objektumot ad vissza,
amely előállítását egy szöveges állomány adatai alapján végzi. Először
megnyitja a megfelelő szöveges állományt olvasásra, ehhez beolvassa az
állomány nevét, és ellenőrizzük, hogy azt jól adták-e meg.

vector<int> FillVector()

ifstream f;

bool error = true;

string str;

do{

cout << "Fájl neve: "; cin >> str;

f.open( str.c_str() );

if ( error = f.fail() ){

cout << "Nincs ilyen fájl!" << endl;

f.clear();

}while(error);

Az állományban elhelyezett első számadat a létrehozandó tömb


hosszát tartalmazza, ezért ezt beolvasva definiálhatjuk a tömböt. Ezt
követően töltjük fel a tömböt az állományban található egész számokkal,
majd lezárjuk az állományt.

int n;

f >> n;

vector<int> t(n);

222
for(int i=0; i<n; ++i){

f >> t[i];

f.close();

return t;

Nullától különböző egész szám beolvasása

Ez tulajdonképpen az előző feladatban bemutatott ReadNat() módosítása,


ahol a beolvasott egész szám vizsgálatánál hibás adatnak nem az n<0
feltételt kielégítő egész számot, hanem a 0==n feltételnek megfelelő számot
tekintjük.

int ReadNotNullInt(const string &msg,

const string &errmsg)

int n;

bool error = true;

string tmp;

do{

cout << msg;

cin >> n;

if(error = cin.fail() || 0 == n){

cout << errmsg << endl;

223
cin.clear();

getline(cin,tmp);

}while(error);

return n;

Kiválogatás

A megoldás harmadik része az absztrakt program kódját tartalmazó


alprogram, amelyet eljárásszerűen hívunk meg. Két, kizárólag bemenő
paraméterváltozója lesz: az egyik a bemenő adatokat tartalmazó tömb, a
másik az oszthatóság vizsgálatához megadott nem nulla egész szám. Az
elsőnek összetett típusa van, ezért annak az értékét konstans referenciaként,
a másodiknak a típusa egyszerű, azt érték szerint fogjuk átvenni.

void Selecting(const vector<int> &t, int k)

for(int i = 0; i<(int)t.size(); ++i){

if(t[i]%k == 0) cout << t[i] << " ";

Tesztelés

A Selecting() teszteléséhez a feladat érvényes fekete doboz


teszteseteit használjuk, külön fehér doboz tesztesetekre nincs szükség:

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

225
Teljes program

#include <iostream>

#include <fstream>

#include <vector>

#include <string>

using namespace std;

vector<int> FillVector();

int ReadNotNullInt(const string &msg,

const string &errmsg);

void Selecting(const vector<int> &t, int k);

int main()

cout << "Adott számmal osztható számok\n";

vector<int> t = FillVector();

int k = ReadNotNullInt("\nKérem az osztót: ",

"Egész számot kérek!\n");

cout << "Eredmény: ";

Selecting(t,k);

return 0;

void Selecting(const vector<int> &t, int k)

226
{

for(int i = 0; i<(int)t.size(); ++i){

if(t[i]%k == 0) cout << t[i] << " ";

int ReadNotNullInt(const string &msg,

const string &errmsg)

int n;

bool error = true;

string tmp;

do{

cout << msg;

cin >> n;

if(error = cin.fail() || 0 == n){

cout << errmsg << endl;

cin.clear();

getline(cin,tmp);

}while(error);

return n;

vector<int> FillVector()

227
{

ifstream f;

bool error = true;

string str;

do{

cout << "Fájl neve: ";

cin >> str;

f.open( str.c_str() );

if ( error = f.fail() ){

cout << "Nincs ilyen fájl!" << endl;

f.clear();

}while (error);

int n;

f >> n;

vector<int> t(n);

for(int i=0; i<n; ++i){

f >> t[i];

f.close();

return t;

228
229
15. Feladat: Páros számok darabszáma

Számoljuk meg egy egész számokat tartalmazó tömb páros elemeit!

Specifikáció

A feladat egy számlálás.

n
A =(t:ℤ ,s:ℕ)
Ef = ( t=t’ )
n
Uf = ( t=t’ s= 1 )
i 1
2 t [i ]

Absztrakt program

A feladat visszavezethető a számlálás programozási tételére úgy, hogy itt az


m..n intervallum helyett 1..n intervallum szerepel és a (i) feltétel a 2 ∣ t[i].

s := 0
i = 1 .. n
2 ∣ t[i]
s := s + 1 SKIP

Implementálás

A tömböt egy olyan szöveges állományból töltjük fel, amely nem tartalmazza
explicit módon a tömb elemeinek számát, elemei elválasztó jelekkel (szóköz,
tabulátor jel, sorvége jel) határolt egész számok, a legutolsó sor végén is van
sorvége jel. Feltesszük, hogy az állomány tartalma helyes, ezért azt nem kell
ellenőriznünk.

230
A tömböt vector<int> típussal fogjuk ábrázolni, amely 0-tól
kezdődően indexelődik, ezért az absztrakt program ciklusváltozója a 0..n-1
intervallumot fogja befutni.
Bevezetünk két függvényt is. Az egyiket az állomány megnyitására és a
tömb feltöltésére (FillVector), a másikat az absztrakt programot
kódolására használjuk.

FillVector()
main()
Count()

6-5. ábra. Alprogramok hívási láncai

A program kerete

Egy forrásállomány (legyen ennek a neve main.cpp) az előző két feladatban


alkalmazott szerkezet szerint épül fel. A main függvény deklarálja a tömböt,
meghívja a FillVector()-t annak feltöltéséhez, majd a Count()-ot az
eredmény kiszámolásához.

#include <iostream>

#include <fstream>

#include <vector>

#include <string>

using namespace std;

void FillVector(vector<int> &t);

int Count(const vector<int> &t);

231
int main()

cout << "Megszámoljuk a páros számokat.\n";

vector<int> t;

FillVector(t);

cout << "A páros elemek száma: " << Count(t);

return 0;

Absztrakt program kódolása

Az absztrakt program kódja alkotja a Count() törzsét.

int Count(const vector<int> &t)

int s = 0;

for(int i = 0; i<(int)t.size(); ++i){

if(t[i]%2 == 0) ++s;

return s;

232
Tömb beolvasása

Ez a kódrész hasonlít az előző feladatban is használt tömb feltöltéshez. Egy


ponton tér el csak attól: most ugyanis azt tételezzük fel a bemenő adatokat
tartalmazó szöveges állományról, hogy nem tartalmazza az adatok számát,
így a tömböt egy fájlvégéig tartó olvasással fokozatosan nyújtjuk megfelelő
méretűre. Ezért a korábbi for ciklus helyett itt egy while ciklus szerepel,
amely az előre olvasási technikát valósítja meg.

void FillVector(vector<int> &t)

ifstream f;

bool error = true;

string str;

do{

cout << "Fájl neve: ";

cin >> str;

f.open( str.c_str() );

if ( error = f.fail() ){

cout << "Nincs ilyen nevű fájl" << endl;

f.clear();

}while (error);

int e;

f >> e;

while(!f.eof()){

233
t.push_back(e);

f >> e;

f.close();

Tesztelés

A Count()fekete doboz teszteléséhez a feladat érvényes teszteseteit


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

234
Teljes program

#include <iostream>

#include <fstream>

#include <vector>

#include <string>

using namespace std;

void FillVector(vector<int> &t);

int Count(const vector<int> &t);

int main()

cout << "Megszámoljuk a páros számokat.\n";

vector<int> t;

FillVector(t);

cout << "A páros elemek száma: " << Count(t);

return 0;

int Count(const vector<int> &t)

int s = 0;

for(int i = 0; i<(int)t.size(); ++i){

if(t[i]%2 == 0) ++s;

235
return s;

void FillVector(vector<int> &t)

ifstream f;

bool error = true;

string str;

do{

cout << "Fájl neve: ";

cin >> str;

f.open( str.c_str() );

if ( error = f.fail() ){

cout << "Nincs ilyen nevű fájl" << endl;

f.clear();

}while (error);

int e; f >> e;

while(!f.eof()){

t.push_back(e); f >> e;

f.close();

C++ kislexikon

236
eljárás-alprogram void Name()

eljárás hívása 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ő void Name(const string &str, int k)

paraméterváltozók {

bemenő és void Name(int &k, bool &l)

eredmény- {
paraméterváltozó

vegyes int Name(int k, bool &l)

l = true;

237
return 5*k;

238
7. Programozási tételek implementálása

A visszavezetésre (lásd első kötet) támaszkodó programtervezés eredménye


egy olyan program, amelyben önálló egységeket alkotnak a programozási
tételekből származtatott programrészek. Egy-egy ilyen programrész
alprogramba szervezhető, és a kódja szabványosítható, azaz kis
odafigyeléssel elérhető, hogy ugyanannak a programozási tételnek a kódja
mindig ugyanúgy nézzen ki.

Implementációs stratégia

A visszavezetés technikájával tervezett programok különféle programozási


tételek mintája szerint épülnek fel, és jól elkülöníthetők bennük az egyes
tételekből származtatott részek. Mivel ezek a részek egy-egy részfeladatot
oldanak meg, ezért mindenképpen érdemes a kódjaikat külön
alprogramokban megadni. Ennek megfelelően a kódban egy programozási
tétel egy alprogram lesz.
Mivel egy programozási tétel egyetlen ciklust tartalmaz, ez az
implementációs stratégia lényegében az egy alprogram-egy ciklus elvét is
kimondja. Természetesen ezt az elvet rugalmasan kell kezelnünk, mert
például egy mátrixban történő számlálásnál, amelynél az elemek bejárását
egy dupla ciklussal végezzük, nem lenne célszerű a belső ciklust külön
alprogramba tenni. Lehetnek olyan esetek is, amikor „több tételnyi kódot”
tartalmaz egyetlen függvény, amely esetleg kiegészül más funkciójú
kódrészekkel, például egy beolvasással.
A programozási tételekből származtatott algoritmusok kódolására
érdemes kialakítani egy olyan stílust, amelyet programjainkban egyfajta
szabványként alkalmazunk. Amilyen következetesen járunk el a visszavezetés
tervezési módszerének alkalmazásában, éppen olyan következetesen
ragaszkodjunk a kialakított kódolási stílushoz. Miért is kellene, mondjuk egy
számlálást mindig másképpen kódolni? Vizsgáljuk meg a választott
programozási nyelv nyújtotta lehetőségeket, válasszuk ki a lehető
legáltalánosabb, futási idejében leggyorsabb kódot, és azt minden
körülmények között következetesen, mint egy kódmintát, alkalmazzuk.

239
Az ilyen önként vállalt vagy a munkahelyünkön kialakított
megállapodására épülő szabvány alkalmazása a kezdeti nehézségeket
leszámítva meggyorsítja a kódolást, jelentősen csökkenti a kódolás során
elkövetett hibákat. A kódunk áttekinthetővé, mások számára is jól
olvashatóvá válik. Ez utóbbi szempont miatt célszerű, ha nem egyedi stílust
alakítunk ki, hanem a programozó szakmában kialakított sémákat utánozzuk.

Összegzés
Számlálás
Maximum kiválasztás
Lineáris keresés
Kiválasztás
Feltételes maximumkeresés

7-1. ábra. Leggyakrabban alkalmazott programozási tételek

A programozási tételek kódolásának szabványosítása nemcsak azt


jelenti, hogy ugyanazt a tételt mindig ugyanúgy kódoljuk, hanem azt is, hogy
a hasonló tételeket, hasonló módon. Például az intervallum elemeinek
bejárására épülő tételek kódjaiban egységesen, ugyanolyan ciklusszervezést
alkalmazunk.
A programozási tételekből származtatott alprogramok
implementálásánál fontos szempont a kód-újrafelhasználás. Ha például a
programunkban több helyen is alkalmazzuk a számlálás programozási tételét,
akár eltérő paraméterek mellett, akkor megfontolandó, hogy a kódban ne
egyetlen számlálást megvalósító alprogramot írjunk-e, amelyet aztán eltérő
paraméterekkel hívhatunk meg. Elsősorban a rendelkezésünkre álló nyelvi
eszközökön múlik, hogy mennyire mehetünk el az általánosítás irányába.
Természetesen ügyeljünk arra, hogy a kód ilyen redundanciáját (hasonló
kódrészek ismétlődését) megszüntető összevonások ne növeljék a program
futási idejét. Próbáljuk megtalálni a helyes egyensúlyt.

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

241
Nyelvi elemek

Egy programozási tételt sokféleképpen kódolhatunk egy programozási


nyelven. C++ nyelven például lehet a ciklust while vagy for utasítással
kódolni, eldönthetjük, hogy akarunk-e speciális ugró utasításokat (break,
continue) alkalmazni. Az is tőlünk függ, hogy egy alprogramba ágyazott
kódrészlet eredménye milyen módon (eredmény-paraméter, visszatérési
érték) jusson vissza a hívás helyére. Ezekre a kérdésekre elég egy, lehetőleg
általános és hatékony megoldást megadni, és aztán ehhez a
megállapodáshoz igazodva kis gyakorlással elérhető legyen, hogy egyazon
tételből származó kódot mindig ugyanúgy írjunk le.
Például a számlálásra mindkét alábbi változat jó, de eldöntjük, hogy
ezen túl következetesen mindig az elsőt fogjuk használni.
int s = 0; int s = 0, i = m;

for(int i=m; i<=n; ++i){ while( i<=n ){

if( felt(i)) ++s; if( felt(i)) ++s;

} ++i;

A stílus uniformizálásának egyik alapja lehet a számlálós ciklus


következetes alkalmazása, mivel a tételek többsége előre meghatározott
számú iterációt végez. Ez alól a lineáris keresés és a kiválasztás programozási
tétele kivétel, mivel ott a ciklus leállása egy speciális feltételtől függ.
Egyébként pont a lineáris keresés az, amelyet meglehetősen sokféleképpen
lehet kódolni. Ennek egyik oka az, hogy már a programozási tételben
szereplő absztrakt algoritmus leírására is több változat létezik, vannak
speciális változatok, de az általános változatnak is több lehetséges leírása. (A
mi változatunkat az első kötetben találjuk.) A másik ok az, hogy azokban a
programozási nyelvekben (pl. Pascal), ahol a for ciklus kizárólag a számlálós
ciklusok kódolására szolgál a lineáris keresést csak elől tesztelős (while)
ciklussal tudjuk kódolni. (Hagyjuk figyelmen kívül a ciklusmagból „goto”
segítségével történő kiugrás lehetőségeit.) A C-szerű nyelvekben azonban a

242
for utasítás általánosabb, mint egy számlálós ciklus, és ezért ott lehetőség
nyílik a korábban említett egységes kódolás érdekében a lineáris keresés
kódolásához is for ciklust használni. Mivel azonban a for utasítás igen
rugalmas, ezért ezzel is többféle verzió készíthető.
Nézzünk két, for utasítást tartalmazó megoldást (ebben a felt(i)
egy itt nem meghatározott, a keresés feltételét vizsgáló logikai értékű
függvény). Mindkettő a lehető legáltalánosabb megoldást nyújtja, de a
jobboldali egy speciális ugró utasítást (break) is tartalmaz. Ennek
végrehajtásakor a vezérlés kiugrik a ciklusból az azt követő utasításhoz.
bool l = false; bool l = false;

int ind; int ind;

for(int i=m; !l && i<=n; ++i){ for(ind=m; ind<=n; ++ind){

l = felt(i); if(l= felt(ind)) break;

ind = i; }

A jobboldali változat talán furcsa a strukturált programozáshoz


végletekig ragaszkodó programozók számára, de az előnye kézenfekvő: míg
baloldali változatban egy külön lokális ciklusváltozó futja be az m..n
intervallumot és az esetlegesen megtalált elem egy másik (ind) változóba
kerül, addig jobboldali változatban az ind változó egyszerre ciklusváltozó és
eredményváltozó is. Ha megszokjuk, hogy kizárólag egy lineáris keresés
kódolásánál használjuk a for ciklust a break utasítással együtt a fenti
formában, akkor ez a fajta strukturálatlanság egyáltalán nem fog problémát
okozni. (Természetesen az ugró utasítások nyakra-főre alkalmazását kerülni
kell, azokat csak jól bejáratott szövegkörnyezetben, valamilyen strukturált
program rövidített leírásaként szabad használni.)
A kiválasztásnál még intervallumról sem beszélhetünk, amelynek
elemeit be kellene futni, mégis alkalmazható a kódolásához a for utasítás.
Arra kell csak figyelni, hogy a ciklusváltozó itt egyben eredményváltozó is,

243
ezért nem a for utasítás hatáskörében kell definiálni azt, hanem még a ciklus
előtt:
int i;

for(i = m; !felt(i); ++i);

A feltételes maximumkeresés (egy f(i) függvénykifejezés értékei


között keresünk felt(i) feltételt kielégítő legnagyobb értéket) kódjában
felhasználhatjuk a C++ nyelv continue ugró utasítását. Ennek hatására a
vezérlés a ciklusmag többi részét átugorja, és a ciklusfeltétel kiértékelésénél
folytatódik. Olyan ez, mintha a ciklusmag többi része a continue-t
tartalmazó if ágnak az else ágában lenne. Ha úgy tekintünk a continue
szerepére, hogy az egy elágazás egyszerűsített formájú leírására szolgál, mert
segítségével nem kell túlságosan egymásba ágyazni a ciklusmag elágazásait,
akkor ezt már a strukturált programozás hívei is használhatják.
bool l = false;

for(int i=0; i<n; ++i){

if (!felt(i)) continue;

if(l){

if(f(i)>max){

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:

244
bool l = false;

for(int i=0; i<n; ++i){

if (felt(i) && l){

if(f(i)>max){

max = f(i); ind = i;

}else if (felt(i) && !l){ {

l = true; max = f(i); ind = i;

Szabványosítást igényel egy-egy programozási tételt tartalmazó


alprogram paraméterezésének módja is. Első pillantásra célravezetőnek
tűnik, ha az eredményt visszatérési értékként adjuk vissza, de ebben erős
korlátozást jelenthet a választott programozási nyelv. Szerencsére a C++
nyelv ebből a szempontból is rugalmas: igaz, hogy csak egy értéket tud egy
függvény visszaadni, de annak tetszőleges lehet a típusa. Ha egyszerre több
eredménye is van egy függvénynek, akkor azokat egy struktúrába összefogva
egy összetett értékként tudjuk visszaadni a hívás helyére. Például a feltételes
maximumkeresésnél az alábbi megoldást választhatjuk (Konkrét esetben a
Value helyére majd a vizsgált elemek olyan típusát kell írni, amelyre
értelmezettek az összehasonlító operátorok.)

struct result{

bool l; // van-e keresett elem

int ind; // legnagyobb keresett elem indexe

Value max; // legnagyobb keresett elem

};

245
result feltmaxker(…);

Mégsem ezt tartjuk a legjobb megoldásnak. Ha több eredménye is van


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

bool linker( … ,int &ind);

246
16. Feladat: Legnagyobb osztó

Határozzuk meg az egynél nagyobb természetes szám önmagától különböző


legnagyobb osztóját.

Specifikáció

A feladat könnyen megoldható, ha a megadott szám felétől elindulunk lefelé


az egész számokon, és az első olyat keressük, amelyik osztja a megadott
számot. Ilyen osztó biztos van, ha más nem, akkor az 1. Ez tehát egy
„biztosan fog találni” jellegű keresés, azaz egy kiválasztás, amelyet nem a
szokásos módon, hanem a számegyenesen lefelé haladva kell alkalmaznunk.

A = ( n : ℕ, d : ℕ )
Ef = ( n=n’ n>1 )

Uf = ( n=n’ d select d n )
d n/2
A feladat másképpen is megoldható. Megkereshetjük a 2..
intervallumban az n szám legkisebb osztóját. Ha ilyen van (legyen ez a d),
akkor a legnagyobb önmagától különböző osztó az n/d lesz. Ha nincs ilyen,
akkor n prím szám, és a legnagyobb önmagától különböző osztója az 1.

n
Uf = ( n=n’ l, k search k n
k 2
(l d = n/k) ( l d = 1) )

Absztrakt program

A feladatot az első esetben a kiválasztás programozási tételére vezethetjük


vissza, de úgy, hogy itt a ciklusmagban csökkenteni kell az eredmény
változót, nem növelni, hiszen a keresést a számegyenesen az n/2-től indulva
balra kell végezni.

247
d := n/2

d := d - 1

A második esetben a lineáris keresés programozási tételére


vezethetjük vissza a megoldásnak azt a részét, amelyik a legkisebb valódi
osztót (k) keresi. Ezt követően még egy elágazásra is szükség van, amely ha a
keresés eredményes volt, akkor n/k alakban kiszámolja a legnagyobb n-től
különböző osztót, különben 1-ként adja meg azt.

,i := hamis,2

i≤
:=
k := i
i := i + 1
l
d := n/k d := 1

Implementálás

A program három részre tagolódik: az adott szám beolvasására, a legnagyobb


nem triviális osztó keresésére és az eredmény kiírására. Ezt a három lépést
ciklikusan, újra és újra végre lehet majd hajtatni.
A main függvényen kívül két függvényt vezetünk be. Az egyik
(ReadInt) egész számok biztonságos beolvasását végzi majd, amelynek meg
lehet paraméterként adni azt, hogy csak az egynél nagyobb egész számokat
fogadja el. A másik a legnagyobb osztó kereséséért (Divisor) felel.
A tervezett ReadInt()-hez hasonló függvényekkel már találkoztunk.
(Lásd előző fejezet ReadNat() vagy ReadNotNullInt() függvényeit.)

248
Ezeket most úgy általánosítjuk, hogy speciális paramétereként meg lehessen
neki adni egy olyan ellenőrző függvényt, amely egész számot vár
bemenetként és egy logikai értéket ad vissza. Ez most a GreaterThanOne()
lesz, amelyik eldönti egy egész számról, hogy az 1-nél nagyobb-e. Ha ez a
feltétel nem teljesül, akkor a beolvasás a megadott számot nem fogadja el.
A Divisor() függvény bemenő adatként kap egy 1-nél nagyobb
természetes számot, visszatérési értékként pedig ennek önmagától
különböző legnagyobb osztóját várjuk tőle.

Függvények hívási láncolata

Jó áttekintést ad a programról az abban használt függvények hívási


kapcsolatrendszere. Ebből kiolvasható, hogy egy függvény melyik másik
függvényt hívja meg. Ezeket a hívási láncokat mutatja az alábbi ábra.

ReadInt() GreaterThanOne()
main()
Divisor()

7-2. ábra. Alprogramok hívási láncai

A hívási láncok mentén adatcsere zajlik az egyes függvények között.


Mivel globális változót nem használunk, a függvények és hívásaik
paraméterlistáiból könnyen kiolvasható, hogy milyen adatok adódnák át a
híváskor, és milyen adat kerül vissza a hívás helyére. A main() egy 1-nél
nagyobb egész számot kap a ReadInt()-től. A ReadInt() ezt a számot
előzőleg még megvizsgáltatja a GreaterThanOne() függvénnyel úgy, hogy
odaadja neki és az egy logikai értékkel jelzi vissza annak helyességét. A
GreaterThanOne() függvényt a main() adja át a ReadInt()-nek. A
Divisor() megkapja a main()-től az ellenőrzött egész számot és
visszaadja annak legnagyobb önmagától különböző osztóját.

249
250
Program keret

A program main függvény előtt találjuk a programban használt függvények


deklarációit. Az All() függvény szerepére később mutatunk rá.

#include <iostream>

using namespace std;

bool GreaterThanOne(int n);

bool All(int n);

int ReadInt(const string &msg, const string &errmsg,

bool check(int) = All));

int Divisor(int n);

A program main függvénye tartalmazza a futtatás végtelenítését


biztosító do-while utasítást, amelynek a törzsében a szám beolvasását végző
ReadInt() függvény meghívására és a számolást végző Divisor()
függvény eredményének kiírására kerül sor.

int main()

cout << "Legnagyobb osztó!\n";

char ch;

string tmp;

do{

int n = ReadInt("A szám: ",

"1-nél nagyobb szám kell!",

251
GreaterThanOne);

cout << "Osztó: " << Divisor(n) << endl;

cout << "Futtassam újra? (I/N)";

cin >> ch; getline(cin, tmp);

}while( ch != 'n' && ch != 'N');

return 0;

A main függvényt a többi függvény definíciója követi.

252
Beolvasás

Egy egynél nagyobb egész szám beolvasásához az előző fejezetben


bemutatott ReadInt() továbbfejlesztését, általánosítását használjuk, ahol
az ellenőrzést egy külön függvény végzi el. Erre az ellenőrző függvényre a
ReadInt() törzsében egy speciális változóval (check) hivatkozunk. Ez a
változó eddig még nem látott módon úgynevezett függvény típusú, azaz egy
függvény adható neki értékül. Nekünk most olyan ellenőrzőfüggvényre van
szükségünk, amely egy egész számot vár bemenő paraméterként és egy
logikai értéket ad vissza, tehát a paraméterváltozónk a bool check(int)
segítségével deklarálható.
Amikor a ReadInt()törzsében a check(…) kifejezés (az
argumentuma helyén egy tetszőleges egész szám értékű kifejezés,
legegyszerűbb esetben egy egész típusú változó állhat) kiértékeléséhez ér a
vezérlés, akkor ez meghívja a check változóban tárolt függvényt az
argumentumában levő kifejezés értékével, majd a függvény által visszaadott
értéket veszi fel.
Mivel a feladatban egynél nagyobb egész számra van szükségünk,
ezért a check változónak olyan ellenőrző függvényt kell értékül adnunk,
amelyik egy egész számra akkor ad igaz választ, ha az nagyobb, mint 1. Ilyen
függvény az alábbi:

bool GreaterThanOne(int n){ return n>1;}

Jól látható, hogy ennek a függvénynek a típusa megegyezik a


ReadInt() harmadik paraméterváltozójának, a check változónak a
típusával. A check paraméterváltozónak alapértelmezés szerinti értéket is
lehet adni. Ez arra szolgál, hogy ha a ReadInt() hívása esetén nem adnánk
meg a harmadik paramétert, akkor az alapértelmezés szerinti függvény
legyen a check -ben tárolt ellenőrző függvény. Kézenfekvő választás erre a
minden egész számot elfogadó All() függvény. Ebben az alkalmazásunkban
ugyan nem hívódik meg ez a függvény, de jelenléte lehetőséget ad a
ReadInt() későbbi általános használatához.

253
bool All(int n) { return true;}

A ReadInt()mindhárom paraméterváltozója egy-egy hivatkozás. Az


első két esetben ezt a & jel mutatja, a harmadik pedig hivatalból az, mert ez
típusa alapján egy ellenőrző függvényre hivatkozik. Ugyanakkor mindhárom
hivatkozás szerinti paraméterváltozó kizárólag bemenő adatforgalmat
bonyolít le. Az első két esetben ezt a const kulcsszó jelzi, a harmadik
esetben csak az önmegtartóztatás, amely nem engedi meg a check változó
megváltoztatását.

int ReadInt(const string &msg, const string &errmsg,

bool check(int)= All)

int n;

int error = true;

string tmp;

do{

cout << msg;

cin >> n;

if(error = cin.fail() || !check(n)){

cout << errmsg << endl;

cin.clear();

getline(cin,tmp);

}while(error);

254
return n;

A ReadInt() működése akkor fejeződik be, ha sikerül megadnia a


felhasználónak egy megfelelő egész számot (mi esetünkben egynél
nagyobbat), és ilyenkor ezzel az egész számmal tér vissza.

255
Absztrakt program kódolása

A Divisor() függvény, attól függően, hogy melyik absztrakt


programot valósítjuk meg, vagy a kiválasztásra épülő változat kódját, vagy a
lineáris keresésre épülő változatét tartalmazza. Mindkét esetben egyetlen
bemenő paramétere lesz: a korábban beolvasott egynél nagyobb egész szám,
a visszatérési értéke pedig a megadott számnak önmagától különböző
legnagyobb osztója.
A kiválasztás:

int Divisor(int n)

int d;

for(d = n/2; n%d != 0; --d);

return d;

A keresés (ehhez szükség van a cmath csomagra):

int Divisor(int n)

int k;

bool l = false;

for(int i = 2; !l && i<=sqrt(n); ++i){

l = n%i == 0;

k = i;

256
}

if(l) return n/k;

else return 1;

257
Tesztelés

Modulonkénti tesztelést végzünk.


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

258
Teljes program

Tekintsük meg a teljes programot.

#include <iostream>

#include <string>

#include <cmath>

using namespace std;

bool GreaterThanOne(int n);

bool All(int n);

int ReadInt(const string &msg, const string &errmsg,

bool check(int) = All);

int Divisor(int n);

int main()

cout << "Legnagyobb osztó!\n";

char ch;

string tmp;

do{

int n = ReadInt("A szám: ",

"1-nél nagyobb szám kell!",

GreaterThanOne);

259
cout << "Osztó: " << Divisor(n) << endl;

cout << "Futtassam újra? (I/N)";

cin >> ch; getline(cin, tmp);

}while( ch != 'n' && ch != 'N');

return 0;

int Divisor(int n)

int d;

for(d = n/2; n%d != 0; --d);

return d;

bool All(int n) { return true;}

bool GreaterThanOne(int n){ return n>1; }

int ReadInt(const string &msg, const string &errmsg,

bool check(int))

260
int n;

int error = true;

string tmp;

do{

cout << msg;

cin >> n;

if(error = cin.fail() || !check(n)){

cout << errmsg << endl;

cin.clear();

getline(cin,tmp);

}while(error);

return n;

261
17. Feladat: Legkisebb adott tulajdonságú elem

Keressük meg egy szöveges állományból feltöltött egész értékű tömbben a


legkisebb olyan számot, amely k-val osztva 1-et ad maradékul! Az állomány
csak egész számokat tartalmaz. A legelső szám a beolvasandó számok
darabszáma, amelyet megfelelő számú (legalább darabszámnyi) egész szám
követ elválasztó jelekkel (szóköz, tabulátor-, sorvége jel) határolva.

Specifikáció

A feladat elvi megfogalmazásában egy tömb a bemenő adat, továbbá három


féle eredmény van: logikai érték, amely jelöli, hogy van-e egyáltalán a
tömbnek k-val osztva 1-et adó eleme, ha igen melyik a legkisebb, és ez
hányadik indexű helyen található.

A = ( t : ℤn, k : ℤ, l : , min, ind : ℤ )


Ef = ( t=t’ k=k’ )
n
Uf = ( t=t’ l , min, ind min t[i]
i 1
t[i ] mod k 1

Absztrakt program

A feladatot a feltételes maximumkeresés programozási tételére vezethetjük


vissza.

l := hamis i: ℤ
i := 1 .. n
t[i] mod k≠1 t[i] mod k=1 l t[i] mod k=1 l
t[i]<min l,min,ind := igaz, t[i], i
SKIP min,ind := t[i],i SKIP

262
Implementálás

A tömböt vector<int> típussal definiáljuk, amelynek elemei 0-tól


indexelődnek, ezért az absztrakt algoritmus ciklusa 0..n–1 intervallumot futja
majd be.
A program most is három részre tagolódik: beolvasás, feldolgozás,
kiírás, amelyek közül az első kettőt önálló alprogramként valósítjuk meg.
Szükségünk lesz egy egész számot beolvasó függvényre és két ellenőrző
függvényre is: az egyik minden egész számot elfogad, a másik csak a nullától
különbözőeket. Ennél fogva a forrásállományban (legyen ennek a neve
main.cpp) a main függvényen kívül még öt másik függvény is megjelenik,
amelyek közül négyet a main függvény törzsében használunk.

int main()

vector<int> t;

Fill(t);

int k = ReadInt("A szám, amivel osztunk: ",

"Nemnulla egész szám kell!",

NotNull);

int min, ind;

if(CondMinSearch(t,k,min,ind))

cout << "A legkisebb keresett szám: " << min

<< "ami a(z) " << ind+1 << "-edik.\n";

else cout <<"Nincs keresett szám!\n";

return 0;

263
Függvények hívási láncolata

A main függvény kódjából jól látható a program függvényeinek hívási


szerkezete. Ezen hívási láncok mentén történik az adatáramlás az egyes
függvények között. A main() egy tömböt kap a Fill() függvénytől, egy
nullától különböző egész számot a ReadInt()-től. A ReadInt() a
beolvasott számot a NotNull() függvénynek adja oda vizsgálatra, amelyik
egy logikai értékkel jelzi vissza annak helyességét. A CondMinSearch()
megkapja a tömböt, a nullától különböző számot, és visszaadja a minimális
adott számmal osztható legkisebb tömbelem értékét és indexét, valamint
egy logikai értéket, hogy volt-e egyáltalán adott számmal osztható elem a
tömbben.

Fill()

main() ReadInt() NotNull()

CondMinSearch()

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,

264
hogy az állomány megfelelően van kitöltve, csak azt vizsgáljuk, hogy az
állomány nevét jól adták-e meg.
Az osztó (k) beolvasását az előző fejezetben bevezetett ReadInt()
függvénnyel végezzük, amelynek most azt kell vizsgálnia, hogy a megadott
szám nullától különböző-e. Ehhez el kell készítenünk az alábbi ellenőrző
függvényt, amelyet a ReadInt() hívásakor annak harmadik paramétereként
kell megadnunk.

bool NotNull(int n){ return n != 1; }

Természetesen a kód tartalmazza a ReadInt() függvény definícióját


és az annak deklarációjában szereplő alapértelmezett paraméterértékként
szereplő All() függvény definícióját is.

Absztrakt program kódolása

Feltételes minimumkeresés kódja a fejezet bevezetőjében elmondottak


alapján készült.

bool CondMinSearch(const vector<int> &t,

int k, int& min, int& ind)

bool l = false;

for(int i=0; i<(int)t.size(); ++i){

if (t[i]%k != 1) continue;

if(l){

if(t[i]<min){ min = t[i]; ind = i;}

}else {l = true; min = t[i]; ind = i;}

265
return l;

Tesztelés

Most is modulonkénti tesztelést végeztünk.


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

266
b. Az osztó lehet 1, páros, páratlan, prím, negatív.
Az érvénytelen tesztadatok a beolvasást végző alprogramok számára
biztosítanak teszteseteket.
A Fill() eljárásnál azonban nem kell érvénytelen adatokra számítani,
mivel a szöveges állományban megállapodás szerint helyesen van kitöltve.
Fehér doboz teszteléséhez meg kell vizsgálni az alábbiakat:
1. Rossz állomány név megadása egymás után többször is.
2. Különböző hosszú tömbök esetei.

A ReadInt()tesztelésénél egyrészt ki kell próbálni, hogy az All()


ellenőrző függvényre jól működik-e, majd hogy a NotNull() mellett is.
Fehér doboz tesztelésnél egymás után többször is érvénytelen adatot kell
megadni.
1. Egész számok (-302, -1, 0, 1, 15) beolvasása.
2. Negatív egész számok esete.
3. Nem egész szám esete.
A main() fekete doboz tesztelésénél a feladat érvénytelen tesztadatait
kellene kipróbálni, de ez itt már nem vezet újabb tesztesetekhez.

267
Teljes program

#include <iostream>

#include <fstream>

#include <vector>

#include <string>

using namespace std;

void Fill(vector<int> &t);

bool CondMinSearch(const vector<int> &t,

int k, int &min, int &ind);

bool NotNull(int n);

bool All(int n);

int ReadInt(const string &msg, const string &errmsg,

bool check(int) = All);

int main()

vector<int> t;

Fill(t);

int k = ReadInt("A szám, amivel osztunk: ",

"Nemnulla egész szám kell!",

NotNull);

int min, ind;

if(CondMinSearch(t,k,min,ind))

268
cout << "A legkisebb keresett szám: " << min

<< "ami a(z) " << ind+1 << "-edik.\n";

else cout <<"Nincs keresett szám!\n";

return 0;

bool CondMinSearch(const vector<int> &t,

int k, int& min, int& ind)

bool l = false;

for(int i=0; i<(int)t.size(); ++i){

if (t[i]<=0) continue;

if(l){

if(t[i]<min){ min = t[i]; ind = i;}

}else {l = true; min = t[i]; ind = i;}

return l;

void Fill(vector<int> &t)

ifstream f;

bool hiba;

string str;

do{

cout << "Fájl neve:";

269
cin >> str;

f.open( str.c_str() );

if ( hiba = f.fail() ){

cout << "Nincs ilyen nevű fájl" << endl;

f.clear();

}while (hiba);

int n;

f >> n;

t.resize(n);

for(int i=0; i<n; ++i){

f >> t[i];

f.close();

bool All(int n) { return true;}

bool NotNull(int n){ return n != 1;}

int ReadInt(const string &msg, const string &errmsg,

bool check(int))

int n;

270
int error = true;

string tmp;

do{

cout << msg;

cin >> n;

if(error = cin.fail() || !check(n)){

cout << errmsg << endl;

cin.clear();

getline(cin,tmp);

}while(error);

return n;

271
18. Feladat: Keressünk Ibolyát

Egy tömb keresztneveket tartalmaz. Van-e a keresztnevek között Ibolya


illetve minden név Ibolya-e?

Specifikáció

Ez itt két különböző feladat, ezért külön-külön oldjuk meg őket. A


specifikációknak azonban csak az utófeltétele tér el egymástól.

A = ( t : String n, l : )
Ef = ( t=t’ )
n
Uf = ( t=t’ l search t[i] " Ibolya " )
i 1
n
Uf = ( t=t’ l search t[i] " Ibolya " )
i 1

Absztrakt program

Az első feladatot az úgynevezett normális (pesszimista) lineáris keresésre, a


másodikat az optimista lineáris keresésre vezetjük vissza.

l, i := hamis, 1 i:ℤ 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

272
elemei 0-tól indexelődnek, ezért az absztrakt algoritmus ciklusa 0..n–1
intervallumot futja majd be.
Eltér viszont a két programban a feldolgozás. Egyikben a
LinSearch(), a másikban az OptLinSearch() kap szerepet, és
természetesen különbözik az eredmény kiírását kísérő szöveg.
Az alábbiakban mindkét változatot megmutatjuk.

Az első változat main függvénye az alábbi:

int main()

vector<string> t;

Fill(t);

if(LinSearch(t)) cout << "Van Ibolya.\n";

else cout << "Nincs Ibolya.\n";

return 0;

A második változat main függvénye pedig az alábbi:


int main()

vector<string> t;

Fill(t);

if(OptLinSearch(t)) cout << "Minden név


Ibolya.\n";

else cout << "Ibolyától eltérő név is


van.\n";

return 0;

273
}

Függvények hívási láncolata

A main egy tömböt kap a Fill() függvénytől. A LinSearch() vagy az


OptLinSearch() megkapja a tömböt, és visszaadja a vizsgálat eredményét
mutató logikai értéket.

Fill()
main()
LinSearch() / OptLinSearch()

7-4. ábra. Alprogramok hívási láncai

Beolvasás

A Fill() eljárás a 15. feladatnál látott FillVector() eljárással majdnem


azonos. Egyetlen különbség ez, hogy most nem egész számokat tartalmazó
tömböt, hanem sztringeket tartalmazó tömböt kell feltöltenünk. A szöveges
állományban elválasztó jelekkel határolva kell megadni a neveket (egy néven
belül nincs elválasztó jel).

void Fill(vector<string> &t)

ifstream f;

bool hiba;

string str;

do{

cout << "Fájl neve:";

cin >> str;

f.open( str.c_str() );

274
if ( hiba = f.fail() ){

cout << "Nincs ilyen nevű fájl" << endl;

f.clear();

}while (hiba);

f >> str;

while(!f.eof()){

t.push_back(str);

f >> str;

f.close();

Absztrakt program kódolása

Nem szorul különösebb magyarázatra a két változat absztrakt programjának


kódolása.

Az első változat:
bool LinSearch(const vector<string> &t)

bool l = false;

for(int i = 0; !l && i<(int)t.size(); ++i){

l = "Ibolya" == t[i];

275
return l;

A második változat:

bool OptLinSearch(const vector<string> &t)

bool l = true;

for(int i = 0; l && i<(int)t.size(); ++i){

l = "Ibolya" == t[i];

return l;

Mindkettőt a lineáris keresés kódolásánál bevezetett minta alapján


készítjük el. Mindkettő a tömböt kapja meg bemenő adatként és egy logikai
értéket ad vissza visszatérési értékként.

276
Tesztelés

A specifikáció alapján felírt érvényes tesztesetek (érvénytelen teszteset most


nincs):
1. Intervallum tesztje:
a. (üres intervallum) Nincs egyetlen név sem. Első változatnál
„Nincs Ibolya”, második változatnál: „Mindenki Ibolya”.
b. (intervallum eleje) Csak az első Ibolya. Első változatnál „Van
Ibolya”, második változatnál: „Van nem Ibolya”.
c. (intervallum vége) Csak az utolsó Ibolya. Első változatnál
„Van Ibolya”, második változatnál: „Van nem Ibolya”.
2. Programozási tétel tesztje:
a. Egy Ibolya név. Első változatnál „Van Ibolya”, második
változatnál: „Mindenki Ibolya”.
b. Egy nem Ibolya név. Első változatnál „Nincs Ibolya”, második
változatnál: „Van nem Ibolya”.
c. Csupa Ibolya név. Első változatnál „Van Ibolya”, második
változatnál: „Mindenki Ibolya”.
d. Csupa nem Ibolya név. Első változatnál „Nincs Ibolya”,
második változatnál: „Van nem Ibolya”.
e. Több név, Ibolya is. Első változatnál „Van Ibolya”, második
változatnál: „Van nem Ibolya”.
3. Különleges értékek tesztje: speciális sztringek a nevek helyén

További teszteseteket csak a Fill() függvény tesztelése ad.


1. Rossz állomány név megadása egymás után többször is.
2. Különböző hosszú tömbök esetei.

277
Teljes program

Tekintsük meg egyben mind a két programot. A Fill() függvényt azonban


nem írjuk le részletesen, csak jelezzük a helyét.

#include <iostream>

#include <fstream>

#include <vector>

#include <string>

using namespace std;

void Fill(vector<string> &t);

bool LinSearch(const vector<string> &t);

int main()

// Adatok beolvasása

vector<string> t;

Fill(t);

// Kiértékelés

if(LinSearch(t)) cout << "Van Ibolya a


tömbben.\n";

else cout << "Nincs Ibolya a


tömbben.\n";

278
return 0;

bool LinSearch(const vector<string> &t)

bool l = false;

for(int i = 0; !l && i<(int)t.size(); ++i){

l = "Ibolya" == t[i];

return l;

void Fill(vector<string> &t)

...

279
A második program:

#include <iostream>

#include <fstream>

#include <vector>

#include <string>

using namespace std;

void Fill(vector<string> &t);

bool OptLinSearch(const vector<string> &t);

int main()

// Adatok beolvasása

vector<string> t;

Fill(t);

// Kiértékelés

if(OptLinSearch(t)) cout << "Minden név


Ibolya.\n";

else cout << "Van Ibolyától eltérő név is.\n";

return 0;

bool OptLinSearch(const vector<string> &t)

280
{

bool l = true;

for(int i = 0; l && i<(int)t.size(); ++i){

l = "Ibolya" == t[i];

return l;

void Fill(vector<string> &t)

...

281
C++ kislexikon

összegzés 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 Value max = f(m); int ind = m;

kiválasztás 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 bool l = true;

keresés int ind;

for(int i=m; l && i<=n; ++i){

l = felt(i);

282
feltételes Value max;

maximumkeresés int ind;

bool l = false;

for(int i=m; i<=n; ++i){

if (!felt(i)) continue;

if(l){

if(t[i]>max){

max = t[i]; ind = i;

}else {

l = true; max = t[i]; ind = i;

283
struktúra struct rekord{

bool l;

int i, ind;

bool u;

};

record r;

r.l = true;

int j = r.ind;

függvény-típusú
paraméter void Name(bool fv(int, const string&))

284
8. Többszörös visszavezetés alprogramokkal

A programtervezés során általában is, de ebben a könyvben különösen


erősen építünk a nevezetes algoritmus mintákra, a programozási tételekre,
amelyekből visszavezetéssel származtatjuk a programjainkat. Már az előző
fejezetben is ilyen programok implementálásával foglalkoztunk, de ott egy
feladat megoldásához egyetlen programozási tételre volt csak szükség. Most
viszont azt vizsgáljuk meg, hogyan érdemes összetett, több egymásba
ágyazott programozási tételre támaszkodó absztrakt programot kódolni.
Egy absztrakt programtervben sokszor találkozhatunk olyan (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

285
előfordulhat a nullával való osztás, sőt az is, hogy ilyenkor nem számot ad
meg a felhasználó. A program ekkor sem „szállhat el”, a hibát észre kell venni
és annak okáról a felhasználót megfelelően kell tájékoztatni. Az ilyen
vizsgálatok következtében elágazások, ciklusok tucatjai épülhetnek be a
kódba, amelynek szerkezete ettől egyre összetettebb lesz. Sokkal
átláthatóbbá válik a kód azonban akkor, ha minden, a tervezésnél kizárt (nem
várt, hibás) esemény bekövetkezése esetén a működést külön „mederbe”
tereljük, amelyet olyan kódrésszel írunk le, amelyet a program többi részétől
elkülönítünk. Ezt a kódolási stílust hívjuk kivételkezelésnek.

Implementációs stratégia

Egy változó deklarációjának helyét úgy választjuk meg, hogy a láthatósága ne


terjedjen túl azon a kódrészen, ahol valóban használni akarjuk. Például egy
for ciklus úgynevezett indexváltozóját – ha tehetjük – a for ciklusban
lokálisan deklaráljuk. Így járjunk el akkor is, ha egymás után több, ugyanolyan
nevű, de egymástól nyilvánvalóan nem függő indexváltozót használó for
ciklust készítünk. Ilyenkor minden egyes ciklusra külön-külön deklarálunk egy
indexváltozót, ezáltal a ciklusok egymástól való függése csökken.
Ha a programterv tartalmaz olyan alprogramokat, amelyek lokális
változókat vezetnek be, akkor ezek a változók a megvalósított kódban is
legyenek lokálisak az alprogramra nézve. Ugyanez igaz a tervben
paraméterváltozóként feltüntetett lokális változókra is; azok legyenek
paraméterváltozók a kódban is. Ettől legfeljebb csak akkor térjünk el, hogy
például az alprogramot a tervvel szemben nem eljárásként, hanem
függvényként akarjuk használni, és ezért néhány eredmény
paraméterváltozó helyett a visszatérési érték fogja az eredményt átadni.
Ekkor az eredmény paraméterváltozó közönséges lokális változó lesz,
amelynek az értékét majd egy return utasítás keretében kell visszaadni.
Általános szabály, hogy ha egy alprogramot függvényszerűen hívunk
meg, akkor lehetőleg ne legyenek eredmény-paraméterváltozói, azaz
ilyenkor a paraméterek csak a bemenő értékeket közvetítsék. Ettől a
szabálytól természetesen indokolt esetben el lehet térni (amennyiben a
programozási nyelv lehetővé teszi). Ilyen indok lehet, a programozási tételek
kódolásánál kialakult hagyomány. Például egy lineáris keresést, amelynek

286
több eredménye is van (talált-e keresett elemet illetve mi a keresett elem)
érdemes úgy kódolni, hogy a találat sikerét jelző logikai értéket visszatérési
értékként, a megtalált elemet paraméterként adjuk vissza. (Elterjedt az
megoldás is, amelyik csak a keresett elemet adja vissza, sikertelen keresés
esetén egy speciális, úgynevezett extremális értékkel.)
Előfordulhat olyan eset, hogy egy alprogram többször is meghívódik,
és a működése egy változójának az alprogram előző hívása során előállított
értékétől is függ. A tervben ez a változó nyilván globális az alprogramra
nézve, és még az alprogram hívásai előtt létre lett hozva. A kódban sem
szabad a változót az alprogram lokális változójaként felvenni, de globális
változóként való létrehozásával kapcsolatban erős ellenérvek vannak.

Lokális változó: Az adott utasításblokkban definiált változó, amely a


definíció kezdetétől a blokk végéig látható, feltéve, hogy egy beágyazott
blokkban nem definiáljuk felül. A beágyazott blokk egy lokális
változójának láthatósága ugyanis elfedi a külső blokk azonos nevű
változójának láthatóságát.
Globális változó: Egy beágyazott utasításblokkban használt olyan
változó, amelyiknek a definíciója a külső blokkban van, és ebből
fakadóan a láthatósága is túlterjed a vizsgált beágyazott blokkon.

8-1. ábra. Program változóinak láthatósági kategóriái

Mindenekelőtt tisztában kell lennünk azzal, hogy lényeges különbség


van a tervezésnél bevezetett globális változó és a programozási nyelvekben
használt globális változó fogalmai között. Többször említettük már, hogy a
programtervben bevezetett változók globálisak a programra nézve:
létrehozásuk és megszüntetésük között a program szabadon használhatja
őket. Ennek megfelelően minden változó, amely egy alprogram hívásakor
még él (azaz korábban jött létre, és még nem szűnt meg) az globális az
alprogramra nézve, tehát az alprogramban is használható. A programozási
nyelvekben a globális változó jóval árnyaltabb fogalom a tervezésnél látott

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

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

289
állhat, de vissza is terelhető a normális működést leíró kódra. Habár nyelvi
szempontból csak ez utolsó, speciális nyelvi elemet igénylő megoldást szokás
kivételkezelésnek nevezni, fogalmilag minden olyan implementációs
megoldás idesorolható, amely a kivételes működés leírására szolgál.

Nyelvi elemek

A több programrészből álló alkalmazások implementálásánál fontos


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

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

291
19. Feladat: Kitűnő tanuló

Egy iskola egyik n diákot számláló osztályában m különböző tantárgyból


osztályoztak a félév végén. A jegyek egy táblázat formájában
rendelkezésünkre állnak. (A diákokat és a tantárgyakat most sorszámukkal
azonosítjuk.) Állapítsuk meg, van-e olyan diák, akinek csupa ötöse van!

Specifikáció

Ennek a feladatnak a megoldását az első kötetben már megterveztük.

A = ( napló:ℕn×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 i n

van := színjeles(i)

i := i+1

van:=színjeles(i)
van,j := igaz,1 j:ℕ

van j m

van := napló[i,j]=5

j := j+1

292
Az absztrakt program második részét (alsó szint) tekinthetjük egy közönséges
részprogramnak, de alprogramként is felfogható. Előbbi esetben a
részprogramot egyszerűen behelyettesítjük a felső szint van:=színjeles(i)
nem-megengedett értékadás helyébe, utóbbiban alprogram-hívásként
tekintünk erre az értékadásra, amely a végrehajtása során átadja a vezérlést
az alprogramnak. Ha a második változat mellett döntünk, akkor azt a
mátrixot, amelyre a fenti tervben globális változóként hivatkozik az
alprogram, elérhetővé kell tenni az alprogram kódjában, és arról is
döntenünk kell, hogy az alprogramot függvényszerű vagy eljárásszerű
hívással aktivizáljuk-e. Ezek olyan nyitott kérdések, amelyeket az
implementáció során kell megválaszolnunk.

Implementálás

A megoldó programban külön függvényben valósítjuk meg az absztrakt


program alprogramját, az osztályzási napló feltöltését, valamint ez utóbbi
által hívott egy egész szám beolvasását végző függvényt, amelyet többféle
ellenőrzési lehetőséggel is fel lehet ruházni. Az absztrakt algoritmus
főprogramja közvetlenül a main függvénybe kerül.

Főprogram

int main()

// Osztályzási napló feltöltése

vector<vector<int> > reg;

ReadMarks(reg);

// Lineáris keresés

bool exists = false;

for(int i=0; !exists && i<(int)reg.size(); ++i){

293
exists = Excellent(reg[i]);

// Eredmény kiírása

if(exists) cout << "Van kitűnő diák.\n";

else cout << "Nincs kitűnő diák.\n";

return 0;

A főprogram három részre tagolódik. Deklarálja a feladat bemenő


(reg) és eredmény (exists) változóját, meghívja a mátrixot feltöltő
ReadMarks() eljárást, amelyet az absztrakt algoritmus főprogramja követ,
végül az eredmény kiírására kerül sor.

Absztrakt alprogram kódolása

A kitűnő diákot vizsgáló alprogramot függvényszerű hívással aktiváljuk,


az eredményt visszatérési értékként adjuk meg. Az alprogramnak nemcsak az
aktuális diák sorszáma (i) a bemenő adata (mint ahogy ezt a programterv
felületes vizsgálata sugallja), hanem szükség van az osztályzatokat tartalmazó
naplóra (reg) is. Valójában a mátrixnak csak az a sora kell, ahol az i-edik diák
jegyei találhatóak. Ezért az alprogram bemenő paramétere a mátrix aktuális
sora lesz, ami egy egydimenziós tömb, és magát a diák sorszámát nem is kell
átadni.

bool Excellent(const vector<int> &v)

bool l = true;

294
for(int j=0; l && j<(int)v.size(); ++j){

l = 5 == v[j];

return l;

Függvények hívási láncolata

Az alábbi kapcsolatrendszerből az olvasható ki, hogy a programban szerepelő


függvények melyik másik függvényeket hívják meg a működésük során.

Nat()
ReadMarks() ReadInt()
main() Mark()
Excellent()

8-2. ábra. Alprogramok hívási láncai

A hívási láncok mentén adatcsere zajlik az egyes függvények között.


(Globális változót nem használunk.) Az osztályzási naplót a billentyűzetről
olvassa be a ReadMarks() eljárás, és egy paraméterváltozó segítségével
adja vissza a főprogramnak. Ennek a mátrixnak az aktuális sorát kapja meg az
Excellent() függvény, amelyik egy logikai értéket ad vissza: igazat, ha a
sor minden eleme ötös, hamisat, ha nem. A ReadMarks() függvény egész
számokat olvas be a ReadInt() segítségével, amelyek egy része (diákok és
tárgyak száma) természetes szám kell legyen, amelyet a logikai értéket
visszaadó Nat() függvény vizsgál; másik része kizárólag 1-től 5-ig terjedő
egész szám lehet, amit a logikai értéket visszaadó Mark() függvény ellenőriz.

Bemenő adatok beolvasása

void ReadMarks(vector<vector<int> > &reg)

295
{

int n = ReadInt("Tanulók száma: ",

"Természetes szám!", Nat);

int m = ReadInt("Tárgyak száma: ",

"Természetes szám!", Nat);

reg.resize(n);

for(int i=0; i<n; ++i){

reg[i].resize(m);

cout << i+1 << ". tanuló eredményei\n";

for(int j=0; j<m; ++j){

cout << "\t" << j+1 << ". tantárgy: ";

reg[i][j] = ReadInt("",

"1 és 5 közötti szám!", Mark);

A ReadMarks()egy n×m-es mátrix méretét határozza meg, majd 1 és


5 közötti egész számokkal feltölti. A mátrixot eredmény paraméterváltozó
segítségével adja vissza (hivatkozás szerinti paraméterátadás). A tervvel
ellentétben a mátrix sorai és oszlopai 0-val kezdődően indexeltek.
Először a mátrix méreteinek bekérésére kerül sor, majd (ehhez a
ReadInt() függvényt hívjuk meg a Nat() ellenőrző függvénnyel), majd a
mátrix méretének beállítása után az elemeit olvassuk be (ReadInt() a
Mark() segítségével).

296
Lényegesen barátságosabb a program, ha a diákokra és a tantárgyakra
a nevükkel lehet hivatkozni, nem sorszámmal.

void ReadMarks(vector<vector<int> > &reg)

int n = ReadInt("Tanulók száma: ",

"Természetes szám!", Nat);

vector<string> student(n);

cout << "Adja meg a tanulók neveit:" << endl;

for(int i=0; i<n; i++) {

cout << i+1 << ". tanuló neve: ";

cin >> student[i];

int m = ReadInt("\nTárgyak száma: ",

"Természetes szám!", Nat);

vector<string> subject(m);

cout << "Adja meg a tantárgyakat:" << endl;

for(int j=0; j<m; j++) {

cout << j+1 << ". tantárgy neve: ";

cin >> subject[j];

reg.resize(n);

for(int i=0; i<n; ++i){

297
reg[i].resize(m);

cout << student[i] << " eredményei\n ";

for(int j=0; j<m; j++) {

cout << "\t" << subject[j] << ":";

reg[i][j] = ReadInt("",

"1 és 5 közötti szám!", Mark);

A ReadInt() a korábbi fejezetekből már ismert függvény, amelynek


harmadik paramétere számára két speciális függvényt is készítünk:
természetes számot (Nat), valamint 1 és 5 közé eső egész számot (Mark)
elfogadó függvényeket.

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.

298
d. Egy diák van egy tárggyal, ami ötös ( azaz van kitűnő diák)
illetve nem ötös (azaz nincs kitűnő diák).
e. Több diák közül csak az első, illetve csak az utolsó kitűnő
(azaz van kitűnő diák).
f. Egy diák több tárggyal, amelyek közül csak az első, illetve
csak az utolsó nem ötös (azaz nincs kitűnő diák).
2. Külső lineáris keresés tesztje:
a. Több diák, több tárgy, és van egy kitűnő, aki az első, vagy az
utolsó, vagy a középső a diákok között.
b. Több diák, több tárgy, és mindenki kitűnő.
c. Több diák, több tárgy, és nincs kitűnő.
3. Belső lineáris keresés tesztje:
a. Egy diák több tárggyal, amelyek közül minden ötös (azaz van
kitűnő diák).
b. Egy diák több tárggyal, amelyek között nem minden ötös
(azaz nincs kitűnő diák).
c. Egy diák több tárggyal, amelyek között csak egy nem ötös
(azaz nincs kitűnő diák).

Ezek az esetek lefedik a main() és az Excellent() fehér doboz


tesztelését is, sőt a ReadMarks() függvényét is.
Az érvénytelen adatok kiszűrését a helyesen paraméterezett
ReadInt() függvény végzi. A ReadInt()tesztelésénél egyrészt ki kell
próbálni, hogy az All() ellenőrző függvényre jól működik-e, majd Nat() és
Mark()mellett is. Fehér doboz tesztelésnél egymás után többször is
érvénytelen adatot kell megadni.
1. Egész számok (-302, -1, 0, 1, 2, 3, 4, 5, 6) beolvasása.
2. Negatív egész számok esete.
3. Nem egész szám esete.
Ennek teszteléséhez ki kell próbálni a helytelen adatok (nem szám,
negatív szám, 5-nél nagyobb vagy 1-nél kisebb osztályzat) beírását.

299
Teljes program

#include <iostream>

#include <vector>

#include <string>

using namespace std;

bool Excellent(const vector<int> &v);

void ReadMarks(vector<vector<int> > &reg);

bool Nat(int n) { return n>=0; }

bool Mark(int n){ return n>=1 && n<=5; }

bool All(int n) { return true; }

int ReadInt(const string &msg, const string &errmsg,

bool check(int) = All);

int main()

// Osztályzási napló feltöltése

vector<vector<int> > reg;

ReadMarks(reg);

300
// Lineáris keresés

bool exists = false;

for(int i=0; !exists && i<(int)reg.size(); ++i){

exists = Excellent(reg[i]);

// Eredmény kiírása

if(exists) cout << "Van kitűnő diák.\n";

else cout << "Nincs kitűnő diák.\n";

return 0;

bool Excellent(const vector<int> &v)

bool l = true;

for(int j=0; l && j<(int)v.size(); ++j){

l = 5 == v[j];

return l;

void ReadMarks(vector<vector<int> > &reg)

301
int n = ReadInt("Tanulók száma: ",

"Természetes szám!", Nat);

vector<string> student(n);

cout << "Adja meg a tanulók neveit:" << endl;

for(int i=0; i<n; i++) {

cout << i+1 << ". tanuló neve: ";

cin >> student[i];

int m = ReadInt("\nTárgyak száma: ",

"Természetes szám!", Nat);

vector<string> subject(m);

cout << "Adja meg a tantárgyakat:" << endl;

for(int j=0; j<m; j++) {

cout << j+1 << ". tantárgy neve: ";

cin >> subject[j];

reg.resize(n);

for(int i=0; i<n; ++i){

reg[i].resize(m);

cout << student[i] << " eredményei\n ";

for(int j=0; j<m; j++) {

cout << "\t" << subject[j] << ":";

reg[i][j] = ReadInt("",

302
"1 és 5 közötti szám!", Mark);

int ReadInt(const string &msg, const string &errmsg,

bool check(int))

int n;

bool error = true;

string tmp;

do{

cout << msg;

cin >> n;

if(error = cin.fail() || !check(n)){

cout << errmsg << endl;

cin.clear();

getline(cin,tmp);

}while(error);

return n;

303
20. Feladat: Azonos színű oldalak

Adott két n oldalú egybevágó szabályos sokszög, amelyek oldalait


véletlenszerűen kiszínezték. Hogyan helyezzük egymásra a két sokszöget úgy,
hogy a lehető legtöbb helyen legyenek azonos színű oldalak egymáson!

Specifikáció

Egy sokszög oldalainak színeit egy n elemű tömbben tároljuk. Így


hallgatólagosan rögzítjük, hogy melyik az első oldal, melyik a második, és így
tovább. A tömböt 0-tól n–1-ig indexeljük, értékei a színek.
Az eredmény egy 0 és n–1 közé eső szám lesz, amely azt mutatja meg,
hogy hányszor kell az egyik sokszöget a másikhoz képest elforgatni az óra
járásával megegyező irányban ahhoz, hogy a lehető legtöbb helyen
kerüljenek azonos színű oldalak egymásra. A forgatás a sokszög oldalainak
újra sorszámozását jelenti. Ha például az x sokszöget i egységgel elforgatjuk,
akkor az így kapott változatnak a j-edik oldala az eredeti változat (i+j) mod n-
edik oldala lesz. Ennek színét kell majd a másik sokszög j-edik oldalának
színével összevetni. Az egyezés(i) azt adja majd meg, hogy az y sokszög hány
oldalának színe egyezik meg az i egységgel elforgatott x sokszög megfelelő
oldalának színével.

A = ( x, y : Szinek 0..n 1 , k:ℕ )


Ef = ( x = x’ y = y’ n > 2 )
n 1
Uf = ( Ef max, k max egyezés (i) )
i 0
ahol egyezés : [0..n–1]
n 1
egyezés(i) = 1
j 0
y[ j ] x[(i j ) mod n]

Absztrakt program

304
A megoldás egy maximum kiválasztásba ágyazott számlálás. A maximum
kiválasztás a különböző i-kre kiszámolt egyezés(i) értékek között keresi a
legnagyobbat, a számlálás pedig az egyezés(i) értékét állítja elő.

max,k := egyezés(0),0

i = 1 .. n–1 i:ℕ

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

305
ReadInt() G2()

main() ReadPoligon()

MaximalFittness() IdenticalEdges()

8-3. ábra. Alprogramok hívási láncai

A main függvény a már korábbról ismert ReadInt() segítségével


olvassa be a sokszögek oldalszámát, ami egy 2-nél nagyobb egész szám. Ezt
ellenőrzi majd a G2() függvény.
Az eljárásként kódolt ReadPoligon() egy sokszöget ad vissza a 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);

306
cout << "Első sokszög oldalainak színei:\n";

ReadPoligon(x);

cout << "Második sokszög oldalainak színei:\n";

ReadPoligon(y);

// Eredmény kiírása

cout << "A második sokszöget "

<< MaximalFittness(x,y)

<< " egységgel kell elforgatni \n" ;

return 0;

Absztrakt program kódolása

A maximum kiválasztás (MaximalFittness()) szokásos eredményei közül


csak az indexet (ind) kell visszaadni, az érték (max) közönséges lokális
változó lesz. A tervben globális változóként szereplő két tömbre bemenő
paraméterváltozókkal hivatkozunk. Ez a függvény nem ellenőrzi, hogy a két
tömb azonos hosszú-e, ezt itt feltételezhetjük.

int MaximalFittness(const vector<string> &x,

const vector<string> &y)

int ind = 0;

int max = IdenticalEdges(x,y,0);

for(int i=0; i<(int)x.size(); ++i){

307
int c = IdenticalEdges(x,y,i);

if(c > max){

max = c; ind = i;

return ind;

A számlálás (IdenticalEdges()) is megkapja paraméterként a két


tömböt valamint az forgatás mértékét, és visszatérési értékként adja majd
meg az illeszkedő oldalak számát.

int IdenticalEdges(const vector<string> &x,

const vector<string> &y,

int i)

int n = (int)x.size();

int c = 0;

for(int j=0; j<n; ++j){

if(x[(i+j)%n] == y[j]) ++c;

return c;

Sokszög beolvasása

308
A ReadPoligon() függvény sztringként olvassa be egy sokszög oldalainak
színét.

void ReadPoligon(vector<string> &t)

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.

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

310
Teljes program

#include <iostream>

#include <vector>

#include <string>

using namespace std;

int MaximalFittness(const vector<string> &x,

const vector<string> &y);

int IdenticalEdges (const vector<string> &x,

const vector<string> &y, int i);

void ReadPoligon(vector<string> &t);

bool G2(int n){ return n>2; }

bool All(int n) { return true; }

int ReadInt(const string &msg, const string &err,

bool check(int) = All);

int main()

//Sokszögek beolvasása

int n = ReadInt("A sokszögek oldalszáma: ",

"2-nál nagyobb egész szám legyen!\n", G2);

vector<string> x(n), y(n);

cout << "Első sokszög oldalainak színei:\n";

311
ReadPoligon(x);

cout << "Második sokszög oldalainak színei:\n";

ReadPoligon(y);

//Eredmény kiírása

cout << "A második sokszöget "

<< MaximalFittness(x,y)

<< " egységgel kell elforgatni.\n";

return 0;

int MaximalFittness(const vector<string> &x,

const vector<string> &y)

int ind = 0;

int max = IdenticalEdges(x,y,0);

for(int i=0; i<(int)x.size(); ++i){

int c = IdenticalEdges(x,y,i);

if(c > max){

max = c; ind = i;

return ind;

312
int IdenticalEdges(const vector<string> &x,

const vector<string> &y,

int i)

int =(int)x.size();

int c = 0;

for(int j=0; j<n; ++j){

if(x[(i+j)%n] == y[j]) ++c;

return c;

void ReadPoligon(vector<string> &t)

for(int i=0; i<(int)t.size(); ++i){

cout << "Az " << i+1 << " oldal színe:";

cin >> t[i];

int ReadInt(const string &msg,

const string &errmsg, bool check(int))

int n;

int error = true;

string tmp;

313
do{

cout << msg;

cin >> n;

if(error = cin.fail() || !check(n)){

cout << errmsg << endl;

cin.clear();

getline(cin,tmp);

}while(error);

return n;

314
21. Feladat: Mátrix párhozamos átlói

Döntsük el, hogy egy adott négyzetes mátrix mindegyik főátlóval párhuzamos
átlójában az elemek összege nulla-e!

Specifikáció

A feladat pontos megfogalmazásához arra lesz szükség, hogy a főátlóval


párhuzamos átlókat ügyesen megsorszámozzuk. Vegyük észre, hogy egy
főátlóval párhuzamos átlóban (ilyen maga a főátló is) az elemek
oszlopindexének és sorindexének különbsége állandó. A főátlóbeli elemek
(t[i,i]) esetén ez a különbség 0, a főátló feletti átlóban 1 (t[i,i+1]), a legfelső
átlóban (t[1,n]) n–1. Közvetlenül a főátló alatti átlóban -1 (t[i,i–1]), a legalsó
átlóban 1–n (t[n,1]). A cél tehát valamelyik olyan átlót megkeresni, amelyik
sorszáma az 1–n .. n–1 intervallum egy eleme. Az összeg(k) a k-adik sorszámú
átló elemeinek összegét adja majd meg.

A = ( t:ℤ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

315
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";

316
return 0;

Megjegyezzük, hogy a main függvényen kívül mindegyik függvény


használja a t mátrixot, a ReadMatrix() feltölti egy állomány alapján, a
többi pedig hivatkozik rá. Itt indokolt lenne a mátrixot globális változóként
bevezetni, de mégsem engedünk a csábításnak. A mátrixot a main
függvényben definiáljuk, és az erre történő hivatkozást adjuk át a másik
három függvénynek.

Függvények hívási láncolata

ReadMatrix()
main()
LinSearch() Summation()

8-4. ábra. Alprogramok hívási láncai

Absztrakt program kódolása

Az optimista lineáris keresést a LinSearch() függvény tartalmazza,


amelynek bemenő paraméterváltozója hivatkozik a mátrixra, amit aztán
tovább is ad az összegzést tartalmazó Summation() alprogramnak. A
LinSearch()eredménye a keresés sikerét jelző logikai érték, amely
visszatérési értékként jut el a hívás helyére.

bool LinSearch(const vector<vector<int> > &t)

int n = (int)t.size();

bool l = true;

for(int k=1-n; !l && k<=n-1; ++k){

317
l = 0 == Summation(t,k);

return l;

A Summation() a mátrixra hivatkozó paraméterváltozó mellett


bemenő paraméterként kapja meg a tervben is paraméterként szereplő átló
sorszámot, és az átló összegét visszatérési értékként adja vissza.

int Summation(const vector<vector<int> > &t, int k)

int n = (int)t.size();

int s = 0;

for(int i=1; i<=n-abs(k); ++i){

s += t[(abs(k)-k+2*i)/2][(abs(k)+k+2*i)/2];

return s;

Bemenő adatok beolvasása

Egy mátrix szöveges állományból való feltöltése már többször szerepelt a


korábbi alkalmazásokban. Ezt tartalmazza a ReadMatrix() eljárás,
amelynek egyetlen bemenő és egyben eredmény paraméterváltozója a
mátrixra való hivatkozás lesz.

void ReadMatrix(vector<vector<int> > &t)

318
{

ifstream f;

string fname;

bool hiba;

do{

cout << "A mátrixot tartalmazó állomány:";

cin >> fname;

f.open(fname.c_str());

if(hiba = f.fail()){

cout << "Hibás állománynév!\n";

f.clear();

}while(hiba);

int n;

f >> n;

t.resize(n);

for(int i=0; i<n; ++i){

t[i].resize(n);

for(int j=0; j<m; ++j) f >> t[i][j];

f.close();

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

enum Errors{ Non_Integer,

Negativ_Matrix_Size,

Not_Enough_Number };

Ha valamelyik hibaeset bekövetkezik, akkor egy annak megfelelő


kivételt dobunk, az eldobott kivételeket pedig a main függvényben kapjuk el
és kezeljük le.

vector<vector<int> > t;

try{

ReadMatrix(t);

}catch(Errors ex){

switch(ex){

320
case Non_Integer:

cout << "Nem egész szám!\n"; break;

case Negativ_Matrix_Size:

cout << "Negatív sor/oszlopok szám!\n";

break;

case Not_Enough_Number:

cout << "Hiányzó adatok a mátrixból!\n";

break;

default:;

exit(1);

Módosítsuk a ReadMatrix() eljárást úgy, hogy ha valamelyik


hibaeset bekövetkezik az olvasás során, akkor dobjon egy Errors típusú
kivételt. Erre a lehetőségre utalhat az eljárás fejében a paraméterlista után
elhelyezett throw (Errors) kifejezés, amely azt jelzi, hogy a függvény
ilyen és csak ilyen típusú kivételt dobhat. Ez azonban igen nagy felelősséget
kíván a programozótól, aki ezzel azt vállalja, hogy minden egyéb kivételt,
amely ennek a kódrésznek a végrehajtásakor keletkezik, lekezel.

void ReadMatrix(vector<vector<int> > &t)

ifstream f;

bool error;

string str;

321
do{

cout << "Fajl neve:";

cin >> str;

f.open( str.c_str() );

if ( error = f.fail() ){

cout << "Nincs ilyen nevu fajl" << endl;

f.clear();

}while (error);

int n = ReadIntFromFile(f);

if(n<0) throw Negativ_Matrix_Size;

t.resize(n);

for(int i=0; i<n; ++i){

t[i].resize(n);

for(int j=0; j<n; ++j){

t[i][j] = ReadIntFromFile(f);

f.close();

A ReadMatrix() eljárás meghív egy segédfüggvényt is, amelyik egy


egész számot próbál beolvasni, és olvasási hiba esetén ez is kivételeket dob.
Ezeket a kivételeket a ReadMatrix() nem kapja el, nem kezeli le, hanem

322
automatikusan tovább dobja, így ezek végül a main függvény
kivételkezelésében csapódnak le.

int ReadIntFromFile(ifstream &f)

string str;

f >> str;

if( f.eof() ) throw Not_Enough_Number;

int n = atoi(str.c_str());

if( 0 == n && str != "0" ) throw Non_Integer;

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.

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

324
Teljes program

#include <iostream>

#include <fstream>

#include <vector>

#include <string>

#include <cmath>

#include <cstdlib>

using namespace std;

enum Errors{ Non_Integer,

Negativ_Matrix_Size,

Not_Enough_Number };

bool LinSearch(const vector<vector<int> > &t);

int Summation(const vector<vector<int> > &t, int k);

void ReadMatrix(vector<vector<int> > &t);

int ReadIntFromFile(ifstream &f);

int main()

// Mátrix létrehozása és kitöltése

vector<vector<int> > t;

try{

ReadMatrix(t);

325
}catch(Errors ex){

switch(ex){

case Non_Integer:

cout << "Nem egész szám!\n"; break;

case Negativ_Matrix_Size:

cout << "Negatív sor/oszlopok szám!\n";

break;

case Not_Enough_Number:

cout << "Hiányzó adatok a mátrixból!\n";

break;

default:;

exit(1);

// Eredmény kiírása

int ind;

if(LinSearch(t,ind))

cout << "Minden átló elemösszege nulla.\n";

else

cout << "Van nem zéróösszegű átló is.\n";

return 0;

bool LinSearch(const vector<vector<int> > &t)

326
{

int n = (int)t.size();

bool l = false;

for(int k=1-n; !l && k<=n-1; ++k){

l = 0 == Summation(t,k);

return l;

int Summation(const vector<vector<int> > &t, int k)

int n = (int)t.size();

int s = 0;

for(int i=1; i<=n-abs(k); ++i){

s += t[(abs(k)-k+2*i)/2][(abs(k)+k+2*i)/2];

return s;

void ReadMatrix(vector<vector<int> > &t)

ifstream f;

bool error;

string str;

do{

327
cout << "Fajl neve:";

cin >> str;

f.open( str.c_str() );

if ( error = f.fail() ){

cout << "Nincs ilyen nevu fajl" << endl;

f.clear();

}while (error);

int n = ReadIntFromFile(f);

if(n<0) throw Negativ_Matrix_Size;

t.resize(n);

for(int i=0; i<n; ++i){

t[i].resize(n);

for(int j=0; j<n; ++j){

t[i][j] = ReadIntFromFile(f);

f.close();

int ReadIntFromFile(ifstream &f)

328
string str;

f >> str;

if( f.eof() ) throw Not_Enough_Number;

int n = atoi(str.c_str());

if( 0 == n && str != "0" ) throw Non_Integer;

return n;

329
C++ kislexikon

kivétel tetszőleges érték vagy objektum

kivétel dobása throw kivétel

kivételt dobó int fv(…) throw (kivétel típusa)

függvény
kivétel figyelése try{

kivétel elkapása és catch(típus változó){

kezelése if(kivétel==változó) …

330
9. Fordítási egységekre bontott program

Egy összetett program kódja jóval áttekinthetőbbé válik, ha a logikailag


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

331
készültek. Magában a csomag törzsében lehetnek olyan részek, amelyek más
csomagok szolgáltatásait igénylik, importálják.

Implementációs stratégia

A kód modulokra bontása, majd bizonyos moduljainak önálló fordítási


egységbe, azaz csomagba zárása kellő tapasztalatot igényel. Csak sok
program megírása után alakul ki egy programozóban az a készség, hogy
milyen logika mentén érdemes egy csomagot kialakítani.
Az egyik rendező elv az azonos célú, hasonló funkciójú alprogramok
egybegyűjtése. Például egy csomagba tehetünk különféle adatbeolvasó
eljárásokat, amelyek közül az egyik egy egész számot, a másik egy valós
számot, a harmadik egy tömböt, stb. olvashat be. Külön csomagba
kerülhetnek ezen eljárásoknak a billentyűzetről olvasó, külön csomagba a
szöveges állományból olvasó változatai. Egy másik példa az, amikor bonyolult
matematikai számításokat végző eljárásokat (mondjuk lineáris
egyenletrendszerek különféle megoldásait vagy integrálszámító numerikus
módszereket) gyűjtünk egy csomagba.

Funkció vezérelt: amikor a hasonló funkciójú alprogramok alkotnak egy


csomagot.
Típus központú: amikor egy adattípus lehetséges műveleteit megvalósító
alprogramok alkotnak egy csomagot.

9-1. ábra. Csomagok kialakításának elvei

Másik rendező elv az, amikor egy bizonyos adattal kapcsolatos


műveleteket gyűjtjük össze az adat elemeit tartalmazó változókkal együtt.
Egy ilyen csomag az adat egyes elemeinek elérését és az azokkal dolgozó
műveleteket biztosítja a külvilág számára, azaz magát az adat típusát írja le
abban az értelemben, ahogy adattípuson az adat lehetséges értékeinek
halmazát és az azokon értelmezett műveleteket értjük. Egy ilyen leírásra az
objektum orientált programozási nyelvek osztály fogalma a legalkalmasabb

332
(ezt a 11. fejezetben mutatjuk be), de egy kezdetleges próbálkozást már e
fejezet 23. feladatának megoldásában is mutatunk.
A csomag az alprogramjain kívül tartalmazhat azok által közösen
használt típus-, változó- és konstans, továbbá úgynevezett belső modulokat
is. Éppen ezért nagyon alaposan meg kell azt gondolni, hogy egy csomag
mely elemeit lehessen a csomagon kívül használni, és amelyek legyenek
azok, amelyek csak belső használatra szolgálnak. Különösen kényes kérdés a
csomagbeli változók külső használatának engedélyezése. Ezek ugyanis olyan
globális változók, amelyek a csomagban is láthatóak és a program azon
részén is, ahol a csomagot használjuk, de nem láthatók más csomagokban. A
globális változók használatával szemben már eddig is megfogalmaztuk a
fenntartásainkat, és ezen most sem kívánunk változtatni.

Export lista: A komponens által nyújtott szolgáltatások, a komponensen


kívül használható konstansok, típusok és meghívható alprogramok
felsorolása.
Import lista: A komponens által használt más komponensekben definiált
szolgáltatások felsorolása.
Törzs: A komponens szolgáltatásainak megvalósítása.

9-2. ábra. Egy komponens részei

Ha egy csomag azokról a szolgáltatásairól, amelyeket az őt használó


programrészek számára nyújt, pontos leírással rendelkezik, azaz szolgáltatás
központú szemlélet alapján készült, akkor a csomagot komponensnek szokás
hívni. Fogalmi szempontból nem túl nagy a különbség a komponens és a
csomag között. Nyelvi szempontból ott húznám meg a határt, amikor
lehetőség van közvetlen módon megadni egy komponens export és import
listáját. A C++ nyelvi eszközei csak közvetett módon teszik ezt, ezért mi
inkább a csomag kifejezést használjuk.
A komponens interfésze (export listája) sorolja fel a komponens
igénybe vehető szolgáltatásait. Ezek csomagon kívül használható változók,

333
típusok, konstansok és alprogramok. A komponens implementációja (törzse)
e szolgáltatások biztosításához szükséges exportált és nem exportált elemek
kódját tartalmazza. Egy komponens maga is támaszkodhat más
komponensek szolgáltatásaira, használhat máshol definiált változókat,
konstansokat, típusokat, alprogramokat, azaz rendelkezik igényeinek,
szükségleteinek gyűjteményével (import listával).

Nyelvi elemek

A több csomagra bontott C++ kód a main függvényt tartalmazó fő-


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

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

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

336
22. Feladat: Műkorcsolya verseny

Soroljuk fel azokat a műkorcsolyázó versenyzőket, akik holtversenyben az


első helyen végeztek a kötelező gyakorlatuk bemutatása után. Az n versenyző
programját m tagú zsűri pontozta, amelyből egy versenyző összesített
pontszámát úgy kapjuk, hogy a legjobb és legrosszabb pontot elvéve a többi
pontszám átlagát képezzük. (A feladat megtalálható az első kötet 6.9.
példájaként.)

Specifikáció

A = ( t : ℝn×m, s : ℕ* )
Ef = ( t=t’ n≥1 m>2 )
n n
Uf = ( Ef s= i max = max pont (i ) )
i 1 i 1
pont( i ) max

ahol pont:[1..n] ℝ
m m m
pont(i) = ( t[i, j ] – max t[i, j ] – min t[i, j ] )/(m–2)
j 1 j 1
i 1

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

337
Ezt megelőzi a versenyzők összesített pontszámait egy tömbben
elhelyező és közben ezek közül a legnagyobb összesített pontszámot is
meghatározó előkészítő rész (max, p inicializálása), amely egy összegzés
(valójában egy összefűzés) és egy maximum kiválasztás ciklusainak
összevonásaként állt elő.

max, p inicializálás d := pont(i)


p[1] := pont(1) o,maxi,mini := t[i,1], t[i,1], maxi,
t[i,1] 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

338
valamilyen értelemben rokon funkciójú (itt mátrix beolvasását és kiírását
végző) függvényeket gyűjtjük egy csomagba.
A tervben szereplő tömbök (t, p) a kódban 0-tól indexelt tömbök
lesznek, ennek megfelelően a tömböket feldolgozó számlálós ciklusok
indextartománya is eggyel eltolódik az absztrakt programokhoz képest.

A program kerete

A vezérlést, azaz az alprogramok megfelelő sorrendben történő meghívását a


main függvény biztosítja.

int main()

vector<vector<double> > t;

Fill(t);

if(t.size()<1 or t[i].size()<3){

cout << "Nem jó méretű a mátrix\n";

return 1;

cout << "Pontszamok: \n";

Write(t);

vector<double> p(t.size());

double max;

Init(t,p,max);

Select(p,max);

339
return 0;

Függvények hívási láncolata

ReadIntFromFile()
Fill()
ReadRealFromFile()
Write()
main()
Init() Result()

Select()

9-3. ábra. Alprogramok hívási lánca

A main függvény kapja meg a Fill() függvénytől a pontszámokat


tartalmazó mátrixot (t), amelyet odaad egyrészt a Write()-nak, hogy az
kiírja, másrészt az Init()-nek, hogy az feldolgozza. Az Init() egyrészt
kiszámolja az egyes versenyzők összpontszámait (ehhez meghívja Result()
függvényt a mátrix egy-egy sorára) és azokat a p tömbben helyezi el,
másrészt meghatározza a maximális összpontszámot (max). Mindkét adatot
visszaadja a main függvénynek, amely továbbadja azokat a Select()-nek. A
Fill() függvény két segédfüggvényt használ a szöveges állományból való
egész illetve valós számok ellenőrzött olvasásához.

Komponens szerkezet

A függvényeket két csomagban helyezzük el. A mátrix feltöltését és kiírását


végző műveleteket a matrix csomagba (matrix.cpp), a többi függvényt a
fő programba (main.cpp).

340
main.cpp matrix.h - matrix.cpp

main() Fill()
Init() Write()
Select() ReadIntFromFile

Result() ReadRealFromFile

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

341
}

A Result() bemenő adata a mátrix egy sora, visszatérési értéke egy


valós szám. A tervben szereplő két elágazás szekvenciája átalakítható egy
háromágú elágazássá (a harmadik ág üres) hiszen a v[j]>max és a v[j]<min
feltételek egymást kizárják.

double Result(const vector<double> &v)

double o, maxi, mini;

o = maxi = mini = v[0];

for(int j=1; j<(int)v.size(); ++j){

o = o + v[j];

if(v[j]>maxi) maxi = v[j];

else if(v[j]<mini) mini = v[j];

return (o-maxi-mini)/(v.size()-2);

A Select()a p valós számokat tartalmazó tömb és a max valós


számot kapja bemenetként.

void Select(const vector<double> &p, double max)

cout << "A legjobb versenyzők:\n";

for(int i=0; i<(int)p.size(); ++i){

342
if(p[i] == max) cout << i+1 << "\t";

Mátrix csomag

Ez a csomag egy mátrix szöveges állományból történő feltöltését és


kiírását biztosítja. A csomag szolgáltatásait (a Fill() és a Write()
alprogramok deklarációit) a matrix.h fejállomány sorolja fel. Ez tehát az
export lista. Mivel itt a vector<> típust használjuk, ezért hivatkoznunk kell a
könyvtári vector csomagra is. Ez a csomagunk import listája. A
fejállományokban nem szokás a using namespace std utasítást
használni, ezért, mivel a vector az std (standard névtér) eleme, a vector
azonosítója elé ki kell írni az std:: minősítést. A fejállományt megfelelő
őrszem-utasítással kell ellátni.

#ifndef MATRIX_H

#define MATRIX_H

#include <vector>

void Fill( std::vector<std::vector<double> > &t);

void Write(

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

343
definíciója található. A Fill() eredmény paramétere és a Write()
bemenő paramétere egyaránt a valós számokat tartalmazó mátrix.
A komponensünk törzsét, a Fill() és a Write() alprogramok kódját
a matrix.cpp tartalmazza.
A mátrix beolvasásánál (Fill()) feltesszük, hogy az adatokat
tartalmazó szöveges állományban először két természetes számot (sorok és
oszlopok száma) találunk, majd azokat követően annyi darab valós számot
(mátrix elemei), amennyi a két természetes szám szorzata. A számokat
legalább egy elválasztó jel (szóköz, tabulátor jel vagy sorvége jel) határolja. Ez
a forma lehetővé teszi például azt, hogy az állomány első sorában a sorok és
oszlopok számát adjuk meg, majd soronként a mátrix egyes soraihoz tartozó
elemeket.

void Fill(vector<vector<double> > &t)

ifstream f;

bool hiba;

string str;

do{

cout << "Fájl neve:";

cin >> str;

f.open( str.c_str() );

if ( hiba = f.fail() ){

cout << "Nincs ilyen nevű fájl" << endl;

f.clear();

}while (hiba);

344
int n, m;

f >> n >> m;

t.resize(n);

for(int i=0; i<n; ++i){

t[i].resize(m);

for(int j=0; j<m; ++j) f >> t[i][j];

f.close();

Módosítsuk a mátrixnak szöveges állományból történő feltöltését úgy,


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

345
hordozó objektum. Mi itt szeretnénk kivételként visszaadni a hibaeset
megnevezését és bizonyos esetekben magát a beolvasott hibás adatot is.
Ezért a kivétel egy rekord (struct) lesz, amelynek egyik tagja a hibaeset
neve, amit egy felsorolt típus értékei közül választhatunk, másik tagja pedig
egy üzenet (string). Négy hibaesetet különböztetünk meg a beolvasás
során előforduló négy féle hiba miatt.

enum Errors{Non_Integer, Non_Real,

Negativ_Matrix_Size, Not_Enough_Number};

struct Exceptions{

Errors code;

std::string msg;

};

Egészítsük ki a Fill() eljárást úgy, hogy ha valamelyik hibaeset


bekövetkezik az olvasás során, akkor dobjon egy kivételt. Ez arra kényszeríti
a komponens használóját (esetünkben a main függvényt), hogy készüljön fel
mindenféle Exceptions típusú kivételt lekezelésére.
Az alprogram eleje nem változik:

void Fill(vector<vector<double> > &t)

ifstream f;

bool hiba;

string str;

do{

cout << "Fájl neve:";

346
cin >> str;

f.open( str.c_str() );

if ( hiba = f.fail() ){

cout << "Nincs ilyen nevű fájl" << endl;

f.clear();

}while (hiba);

Egy kivételnek ki kell tölteni a hibakód- és az üzenet mezőjét. Az


üzenetbe célszerű beleírni azt az adatot is, amely a kivétel dobását kiváltotta.
Esetünkben az adatok számok, amelyeket sztringgé kell konvertálnunk. Az
alábbi kódban ezt egy ostringstream típusú objektum segítségével
végezzük el. Ehhez szükségünk van az sstream csomagra.

int n = ReadIntFromFile(f);

if(n<0) {

Exceptions ex;

ex.code = Negativ_Matrix_Size;

ostringstream ss;

ss << n;

ex.msg = ss.str();

throw ex;

int m = ReadIntFromFile(f);

if(m<0){

347
Exceptions ex;

ex.code = Negativ_Matrix_Size;

ostringstream ss;

ss << m;

ex.msg = ss.str();

throw ex;

t.resize(n);

for(int i=0; i<n; ++i){

t[i].resize(m);

for(int j=0; j<m; ++j){

t[i][j] = ReadRealFromFile(f);

f.close();

A Fill() eljárás meghív két segédfüggvényt is:


ReadIntFromFile() és ReadRealFromFile(). Egyik egy egész, másik
egy valós számot próbál beolvasni. Ezekre a segédfüggvényekre a
főprogramban nincs közvetlenül szükség, a matrix komponens nem is
ajánlja fel őket külön szolgáltatásként, hiszen nem szerepelnek az export
listában, azaz a matrix.h-ban. Ezek a függvények is dobnak Exceptions
típusú kivételt, amelyet a Fill() nem kezel le, hanem tovább dob.

int ReadIntFromFile(ifstream &f)

348
{

string str;

f >> str;

if( f.eof() ){

Exceptions ex;

ex.code = Not_Enough_Number;

throw ex;

int n = atoi(str.c_str());

if( 0 == n && str != "0" ){

Exceptions ex;

ex.code = Non_Integer;

ex.msg = str;

throw ex;

return n;

Olvasási hiba esetén mindkét segédfüggvény kivételeket dobhat,


amelyeket azonban a Fill() nem kezel le, hanem automatikusan
továbbdobja az őt hívó programrésznek. Ezek éppen olyan Exception
típusú kivételek, mint amelyeket a Fill() amúgy is dobni tud.

double ReadRealFromFile(ifstream &f)

349
string str;

f >> str;

if( f.eof() ){

Exceptions ex;

ex.code = Not_Enough_Number;

throw ex;

double a = atof(str.c_str());

if( 0 == a && str != "0" ){

Exceptions ex;

ex.code = Non_Real;

ex.msg = str;

throw ex;

return a;

A main függvényben kell elkapnunk és lekezelnünk a Fill() által


dobható kivételeket. Ott ahol a kivétel adatat is lényeges, nemcsak a
hibaeset nevát, de az üzenetét is kiírjuk. Láthatjuk, hogy külön ellenőrizzük,
hogy a mátrix legalább 1×3-as legyen.

vector<vector<double> > t;

try{

Fill(t);

350
}catch(Exceptions ex){

switch(ex.code){

case Non_Integer:

cout << "Rossz formájú egész szám!"

<< ex.msg << endl; break;

case Non_Real:

cout << "Rossz formájú valós szám!"

<< ex.msg << endl; break;

case Negativ_Matrix_Size:

cout << "Sor, oszlop szám nem negatív!"

<< ex.msg << endl; break;

case Not_Enough_Number:

cout << "Hiányzó adatok!\n"; break;

default:;

exit(1);

if(t.size()<1){

cout<<"Legalább 1 versenyző kell!"; exit(1);

if(t[0].size()<3){

cout<<"Legalább 3 zsűritag kell"; exit(1);

351
A csomag másik eleme, a Write(), egy valós számokat tartalmazó
mátrixot ír ki.

void Write(const vector<vector<double> > &t)

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

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

353
354
Teljes program

main.cpp:

#include <iostream>

#include <vector>

#include <cstdlib>

#include "matrix.h"

using namespace std;

void Init(const vector<vector<double> > &t,

vector<double> &p,double &max);

void Select(const vector<double> &p, double max);

double Result(const vector<double> &v);

int main()

vector<vector<double> > t;

try{

Fill(t);

}catch(Exceptions ex){

switch(ex.code){

case Non_Integer:

cout << "Rossz formájú egész szám!"

355
<< ex.msg << endl; break;

case Non_Real:

cout << "Rossz formájú valós szám!"

<< ex.msg << endl; break;

case Negativ_Matrix_Size:

cout << "Sor, oszlop szám nem negatív!"

<< ex.msg << endl; break;

case Not_Enough_Number:

cout << "Hiányzó adatok!\n"; break;

default:;

exit(1);

if(t.size()<1){

cout<<"Legalább 1 versenyző kell!"; exit(1);

if(t[0].size()<3){

cout<<"Legalább 3 zsűritag kell"; exit(1);

cout << "Pontszamok: \n";

356
Write(t);

vector<double> p(t.size());

double max;

Init(t,p,max);

Select(p,max);

return 0;

void Init(const vector<vector<double> > &t,

vector<double> &p, double &max)

p[0] = Result(t[0]);

max = p[0];

for(int i=1; i<(int)t.size(); ++i){

p[i] = Result(t[i]);

if(p[i]>max) max = p[i];

void Select(const vector<double> &p, double max)

cout << "A legjobb versenyzők:\n";

for(int i=0; i<(int)p.size(); ++i){

if(p[i] == max) cout << i+1 << "\t";

357
}

double Result(const vector<double> &v)

double o, maxi, mini;

o = maxi = mini = v[0];

for(int j=1; j<(int)v.size(); ++j){

o = o + v[j];

if(v[j]>maxi) maxi = v[j];

else if(v[j]<mini) mini = v[j];

return (o-maxi-mini)/(v.size()-2);

358
matrix.h:

#ifndef MATRIX_H

#define MATRIX_H

#include <vector>

#include <string>

enum Errors{Non_Integer, Non_Real,

Negativ_Matrix_Size, Not_Enough_Number};

struct Exceptions{

Errors code;

std::string msg;

};

void Fill( std::vector<std::vector<double> > &t);

void Write(

const std::vector<std::vector<double> > &t);

#endif

matrix.cpp:
#include "matrix.h"

#include <iostream>

#include <cstdlib>

#include <fstream>

359
#include <sstream>

using namespace std;

int ReadIntFromFile(ifstream &f);

double ReadRealFromFile(ifstream &f);

void Write(const vector<vector<double> > &t)

for(int i=0; i<(int)t.size(); ++i){

for(int j=0; j<(int)t[i].size(); ++j){

cout << t[i][j] <<"\t";

cout << endl;

void Fill(vector<vector<double> > &t)

ifstream f;

bool hiba;

360
string str;

do{

cout << "Fájl neve:";

cin >> str;

f.open( str.c_str() );

if ( hiba = f.fail() ){

cout << "Nincs ilyen nevű fájl" << endl;

f.clear();

}while (hiba);

int n = ReadIntFromFile(f);

if(n<0) {

Exceptions ex;

ex.code = Negativ_Matrix_Size;

ostringstream ss;

ss << n;

ex.msg = ss.str();

throw ex;

int m = ReadIntFromFile(f);

if(m<0){

Exceptions ex;

361
ex.code = Negativ_Matrix_Size;

ostringstream ss;

ss << m;

ex.msg = ss.str();

throw ex;

t.resize(n);

for(int i=0; i<n; ++i){

t[i].resize(m);

for(int j=0; j<m; ++j){

t[i][j] = ReadRealFromFile(f);

f.close();

int ReadIntFromFile(ifstream &f)

string str;

f >> str;

if( f.eof() ){

362
Exceptions ex;

ex.code = Not_Enough_Number;

throw ex;

int n = atoi(str.c_str());

if( 0 == n && str != "0" ){

Exceptions ex;

ex.code = Non_Integer;

ex.msg = str;

throw ex;

return n;

double ReadRealFromFile(ifstream &f) {

string str;

f >> str;

if( f.eof() ){

Exceptions ex;

ex.code = Not_Enough_Number;

throw ex;

363
double a = atof(str.c_str());

if( 0 == a && str != "0" ){

Exceptions ex;

ex.code = Non_Real;

ex.msg = str;

throw ex;

return a;

364
23. Feladat: Melyikből hány van

Olvassunk be a billentyűzetről egész számokat, majd mondjuk meg, melyik


szám hányszor szerepelt.

Specifikáció

A billentyűzetről érkező egész számok sorozatára a specifikációban a t


változóval hivatkozunk. Célunk egy olyan s sorozat, egy úgynevezett tároló
előállítása, amely érték-darabszám párokat tartalmaz, ahol egy érték egy t-
beli elem, a hozzátartozó darabszám pedig annak t-beli előfordulási száma.
Ugyanaz a t-beli elem csak egyszer szerepelhet az s-ben.
A = ( t : ℤ*, s : Pár* ) Pár = rec(érték : ℤ , darab : ℕ)
Ef = ( t=t’ )
t'
Uf = ( Ef s= t' k )
k 1

ahol művelet befűz az s sorozatba egy e egész számot úgy, hogy ha az már
szerepel az s-ben (ehhez egy lineáris keresést kell az s értékeire alkalmazni),
akkor csak annak darabszámát növeli meg, ha még nem, akkor felveszi az s-
be 1 darabszámmal. Itt kibontakozik egy s := s e részfeladat, amelynek
specifikációja:
A = ( s : Pár*, e : ℤ )
Ef = ( s=s’ e=e’ )
s'
Uf = (e=e’ l , ind search( si .érték e)
i 1
(l sind.darab = s’ind.darab+1) ( l s = s’ <n,1>) )

Absztrakt program

s := <>

k = 1 .. t k:ℕ

s := s tk

365
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

Store()
main()
Write()
9-5. ábra. Alprogramok hívási lánca

366
A main függvény az absztrakt főprogramot tartalmazza, amely a
billentyűzetről olvassa a képzeletbeli t sorozat elemeit, elhelyezi azokat az s-
ben a Store() segítségével, majd kiírja az s tartalmát a Write()-tal. A
Store() valósítja meg az s:=s e alprogramot, a bemenetként kapott e
elemet elhelyezi az s sorozatban. A Write() az s elemeinek a konzolablakra
történő kiírását végzi.

Komponens szerkezet

A függvényeket két csomagban helyezzük el. Külön csomagot alkot a main


függvény – ez lesz a main.cpp-ben –, és külön csomagba kerül a tárolót
megvalósító kód. Mivel magát az s tárolót nem exportáljuk, a container.h
fejállomány ezért csak a két művelet (Store() és Write()) deklarációját
tartalmazza. Az s típusának definíciója és a két művelet alprogramjának
törzse a container.cpp állományba kerül.

main.cpp container.h - container.cpp

Store()
main()
Write()

9-6. ábra. Komponens szerkezet

A container csomag így egyetlen tárolót definiál csak. Ez nem


alkalmas arra, hogy a leírását típusként értelmezve, több ilyen típusú
tárolót hozzunk létre. (Ezt majd csak az osztály segítségével tudjuk
megtenni.) Most azonban egyetlen tároló is elég.

367
Főprogram

A main függvény az absztrakt főprogramot kódolja:

int main()

cout << "Adjon meg egész számokat!\n”

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

368
};

static vector<Pair> s;

Amennyiben a static kulcsszót nem alkalmaznánk, akkor


módunkban állna, hogy a main.cpp-ben egy extern vector<Pair> s
deklarációt követően közvetlenül hozzáférjünk az s adataihoz. Ezt azonban
szeretnénk elkerülni, és csak a Store() és a Write() műveleteken
keresztül akarjuk használni az s sorozatot.
A Store() megvalósítása a programterv s:=s e alprogramja alapján
készül. Vegyük észre, hogy az eljárás feje nem utal arra, hogy ez az alprogram
az s sorozatot be és kimenő adatként egyaránt használja. Ez az egyik oka
annak, hogy a most bemutatott implementációs technikát nem tartjuk
kielégítőnek, helyette inkább objektum orientált nyelvi eszközt, az osztályt
használjuk szívesebben.

void Store(int e)

bool l = false;

int ind;

for(int i=0; !l && i<(int)s.size(); ++i){

l = s[i].value == e;

ind = i;

if(l) ++s[ind].no;

else{

Pair p; p.value = e; p.no = 1;

s.push_back(p);

369
}

A Write() művelet a tároló elemeit kiíró eljárás.

void Write()

for(int i=0; i<(int)s.size(); ++i){

cout << s[i].value << " "

<< s[i].no << endl;

Tesztelés

Fekete doboz tesztesetek: (Az egyes programozási tételek tesztjét


összevonjuk a tételekben szereplő intervallum tesztjével.)
1. A main()-beli összegzés tesztje:
a. Egyetlen számot sem adunk meg.
b. Több számot adunk meg.
2. A Store()-beli lineáris keresés tesztje:
a. Egymás után két különböző számot adunk meg.
b. Egymás után két vagy több azonos számot adunk meg.
c. Nem közvetlenül egymás után adunk meg két azonos
számot.
Modulonkénti tesztelés:
Store()- Az előző tesztesetek lefedik a modul tesztelését (első elem
elhelyezése, a tároló tömbben még nem szereplő elem elhelyezése, létező

370
elem ismételt elhelyezése a tároló tömb elején illetve végén), ugyanakkor a
modul nem kezeli a memória elfogyásnál bekövetkező eseményt.
Write()- Az eddigi tesztesetek lefedik ennek a modulnak a tesztelését.

Teljes program

main.cpp:

#include <iostream>

#include <fstream>

#include <vector>

#include "container.h"

using namespace std;

int main()

cout << "Adjon meg egész számokat!\n”

<< "Betűvel jelezze a bevitel végét!\n";

int n;

while(cin >> n){

Store(n);

cout << "\nA számok és az előfordulásuk:\n";

Write();

371
return 0;

372
container.h:

#ifndef CONTAINER_H

#define CONTAINER_H

#include <vector>

void Store(int e);

void Write();

#endif

container.cpp:
#include "container.h"

#include <iostream>

using namespace std;

struct Pair{

int value;

int no;

};

static vector<Pair> s;

373
void Store(int e)

bool l = false;

int ind;

for(int i=0; !l && i<(int)s.size(); ++i){

l = s[i].value == e;

ind = i;

if(l) ++s[ind].no;

else{

Pair p; p.value = e; p.no = 1;

s.push_back(p);

void Write()

for(int i=0; i<(int)s.size(); ++i){

cout << s[i].value << " "

<< s[i].no << endl;

C++ kislexikon

374
állomány-őrszem #ifndef NEV

#define NEV

#endif

csomagra
korlátozott globális static int x;
változó
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.

375
10. Rekurzív programok kódolása

Ez a fejezet egy kakukktojás. Könyvünk első kötetében, a tervezésnél, ugyanis


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

Implementációs stratégia

Mindig a tervezési fázisban dől el az, hogy egy feladat megoldására


rekurziót alkalmazunk-e, illetve hogy azt rekurzív programmal
kódoljuk-e. Itt rögtön különbséget kell tudnunk tenni három fogalom

376
között. Egy feladat leírása, specifikálása is történhet rekurzív
függvények segítségével (erre láthattunk példát az első kötetben),
adhatunk egy feladatra rekurzív algoritmus képében absztrakt
megoldást, és kódolhatjuk azt annak szerkezetéhez hűen ragaszkodva
rekurzív formában, de át is alakíthatjuk azt nem-rekurzív algoritmussá.
Például egy természetes szám faktoriálisát kiszámíthatjuk
összeszorzásokkal az összegzés programozási tétele alapján (lásd 6.
fejezet), ahol sem a megoldás ötlete, sem a megoldó algoritmus, sem
annak kódja nem tartalmaz rekurziót. Támaszkodhatunk azonban az
ismert n! = n*(n–1)! (és 1! = 1) rekurzív képletre is, amelyre
alkalmazhatjuk a rekurzív függvény értékét kiszámító, azaz 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)

n>1
f := n*Fact(n-1) f := 1

Azt, hogy ez az alprogram a rekurzív képletnek felel meg, nem


kell bizonygatni. Ezért, amennyiben a képlet helyes, az alprogramot
meghívó program az alprogramtól a kívánt eredmény kapja majd
vissza. De vajon biztosan leáll-e ez az alprogram? Egy rekurzív
alprogram leállásának szükséges feltétele az, hogy legyen olyan
végrehajtása is, amely nem hívja meg újra önmagát. Ehhez legalább
egy elágazásra vagy egy ciklusra van szükség az alprogramban. A fenti
példában az elágazás jobboldali ága nem tartalmaz rekurzív hívást. A
leállás másik feltétele, hogy ne fordulhasson az elő, hogy az alprogram
mindig újabb rekurzív hívást hajtson végre, azaz véges számú rekurzív

377
hívása után bizonyosan a rekurziómentes ágra fusson rá a vezérlés. A
fenti alprogram minden egyes hívásával eggyel kisebb értéket ad az n
paraméterváltozónak. Ez a garancia arra, hogy véges számú hívás után
az 1 értékkel hívódik meg az alprogram, amikor is az már nem fog
újabb rekurzív hívást kezdeményezni.
Egy rekurzív alprogram lokális változójának annyi példánya lesz,
ahányszor az alprogramot meghívják. A fenti példában nincs értelme
feltenni azt a kérdést, hogy mennyi az n változó értéke, mert nem
beszélhetünk egyetlen n változóról. A vezérlés szempontjából azonban
mindig pontosan megállapítható az alprogram legutoljára meghívott
és még be nem fejezett változatának változói, és ezek értéke
egyértelműen meghatározható.
Minden rekurzív program átalakítható nem-rekurzív programmá
(sőt fordítva is). Az átalakítást többnyire a hatékonyság javításának
céljából tesszük meg, bár ilyenkor azt is mérlegelni kell, hogy az így
nyert program átláthatósága, és ennek következtében az érthetősége,
javíthatósága mennyire romlik a rekurzív változathoz képest. Egy
rekurzív program nagy előnye ugyanis a tömör, áttekinthető leírás.
Vannak olyan programozási nyelvek is, amelyek nem támogatják a
rekurzív alprogramok készítését, és ilyenkor a nem-rekurzív átalakítás
az egyetlen járható út.

Nyelvi elemek

A rekurzív alprogramok kódolásának nyelvi eszközei ugyanazok, mint


amelyeket a közönséges alprogramok kódolásánál megismertünk.
Fel kell azonban hívni még egyszer arra a figyelmet, hogy egy
alprogram minden hívása után a lokális változói újból létrejönnek és az adott
hívás befejeződéséig a verem memóriában maradnak. Az ismétlődő rekurzív
hívások ugyanazon lokális változók újabb és újabb példányait hozzák létre a
verem memóriában. Az új lokális változóknak a név- és típusegyezésen kívül
semmi kapcsolatuk sincs a korábbi meghívásakor létrejött lokális változókkal.
A verem memóriában egyidejűleg léteznek az alprogram korábbi és újabb

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

379
24. Feladat: Binomiális együttható

Számoljuk ki a k-adik n-ed rendű binomiális együtthatót, az -t!

Specifikáció

A feladat specifikációja igen egyszerű.

A = ( n,k,b : ℕ )
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
n 2, k [1..n–1]:

380
A másik definíció az úgynevezett Pascal háromszög törvényszerűségét
használja ki:
n ℕ: és

n 2, k [1..n–1]:

Absztrakt program

Mindkét rekurzív képlethez elkészítjük az absztrakt rekurzív programot, de


végül az elsőt fogjuk implementálni. Mindkét rekurzív program
helyettesíthető egy nem-rekurzív változattal, a második megoldás egy igen
egyszerűvel, de mivel ebben a fejezetben a rekurzív hívású alprogramokkal
foglalkozunk, ezeket a változatokat nem mutatjuk be.
Az első rekurzív képletnek megfelelő absztrakt algoritmus legfeljebb n-
szer hívja meg önmagát.

b := Binomial(n,k)
k=0
b := 1 b := Binomial(n,k–1)* (n–k+1)/k

Ennek megvalósításnál ügyelni kell arra, hogy először az n–k+1-val


történő szorzást végezzük el és utána a k-val való osztást, hogy ne
keletkezzen részeredményként törtszám, mert a törtrészek elvesztésével
sérülne a végeredmény. Sajnos emiatt előfordulhat, hogy amikor az
eredmény még elférne ugyan az eredmény változónak a megvalósításnál
kijelölt típusa által meghatározott memória területén, a számolás
részeredménye már túlcsordul azon.
Ezért az implementációhoz a második változatot használjuk fel.

b := Binomial(n,k)

n=0 k=0 k=n

381
b := 1 b := Binomial(n–1,k–1)+Binomial(n–1,k)

n
Igaz, hogy ennek idő hatékonysága rosszabb, az kiszámolásához akár 2 -
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.

main.cpp read.h - read.cpp


main() ReadInt()
Binomial() Nat()
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.

ReadInt()
main() Nat()
Binomial()

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

383
A fő program

A binomiális együttható kiszámolását ciklikusan ismételhető módon


valósítjuk meg a main függvényben.

int main()

char ch;

do{

int n = ReadInt("n= ",

"Természetes szám kell!\n",Nat);

int k = ReadInt("k= ",

"Természetes szám kell!\n",Nat);

cout << "B(n,k)= " << Binomial(n,k) << endl;

cout << "Folytatja? (I/N): "; cin >> ch;

}while(ch != 'n' && ch != 'N');

return 0;

Rekurzív függvény

A Binomial() függvényt a terv alapján kódoljuk.

int Binomial(int n, int k)

384
if(0 == n || 0 == k || k == n) return 1;

else return Binomial(n-1,k-1)+Binomial(n-1,k);

385
Tesztelés

Fekete doboz tesztesetek:

1. =1

2. =1

3. =1

4. =1

5. =1
6. Általános esetek.
7. Skálázás. (Annak kimérése, hogy legfeljebb mekkora n és k értékekre
tudjuk kiszámolni az eredményt, és ez mennyire tart sokáig.)

Modul tesztek:
main()- A ciklikusan ismételhetőség kipróbálása.
ReadInt(),Nat()- Ezeket a függvényeket korábban már többször
használtuk, a tesztelésük ott megtalálható.
Binomial()- A fekete doboz tesztesetek lefedik a tesztelést.

386
Teljes program

main.cpp:
#include <iostream>

#include "read.h"

using namespace std;

int Binomial(int n, int k);

int main()

char ch;

do{

int n = ReadInt("n= ",

"Természetes számot kérek!\n",Nat);

int k = ReadInt("k= ",

"Természetes számot kérek!\n",Nat);

cout << "B(n,k)= " << Binomial(n,k) << endl;

cout << "Folytatja? (I/N): "; cin >> ch;

}while(ch != 'n' && ch != 'n');

return 0;

387
int Binomial(int n, int k)

if(0 == n || 0 == k || k == n) return 1;

else return Binomial(n-1,k-1)+Binomial(n-1,k);

read.h:
#ifndef READ_H

#define READ_H

#include <string>

bool ci(int k);

bool Nat(int n);

int ReadInt (std::string msg,

std::string errormsg,

bool cond(int) = ci);

#endif

read.cpp:
#include "read.h"

#include <iostream>

388
using namespace std;

bool ci(int k){ return true;}

bool Nat(int n) { return n >= 0; }

int ReadInt(string msg,

string errormsg, bool cond(int) )

int n;

int hiba = true;

string tmp;

do{

cout << msg;

cin >> n;

if(cin.fail() || !check(n)){

cout << errmsg << endl;

cin.clear();

getline(cin,tmp);

}while(hiba);

return n;

389
25. Feladat: Hanoi tornyai

Adjunk megoldást a Hanoi tornyai problémára! Ebben a játékban több


különböző méretű lyukas korong helyezkedik el három rúd valamelyikén.
Egyszerre mindig csak egy korong rakható át egy másik rúdra, de úgy, hogy
soha nem tehetünk nagyobb korongot kisebb korong tetejére. Kezdetben
minden korong az 1-es sorszámú rúdon található. Milyen mozgatásokkal
vihető át az összes korong a 3-as rúdra?

Specifikáció

Egy korong mozgatása két rúd-sorszámmal (honnan-hova) adható meg. Egy


ilyen mozgatást leíró (i,j) számpárt a továbbiakban a szemléletesség kedvéért
(i j) alakban fogjuk írni. A cél a megfelelő mozgatás-sorozat előállítása.

A = ( n : ℕ , 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)

390
Implementálás

A megoldó programot közvetlenül, rekurzívan hívható függvény segítségével


kódoljuk.

ReadInt()
main() Nat()
Hanoi()
10-3. ábra. Alprogramok hívási láncai

Az alkalmazás szerkezete szinte szó szerint megegyezik az előző feladat


megoldáséval.

main.cpp read.h - read.cpp


main() ReadInt()
Hanoi() Nat()
10-4. ábra. Komponens szerkezet

A fő program

A probléma megoldását ciklikusan ismételhető módon valósítjuk meg a main


függvényben.

int main()

char ch;

do{

int n = ReadInt("n= ",

"Természetes számot kérek!\n",Nat);

391
cout << Hanoi(n,1,3,2) << endl;

cout << "Folytatja? (I/N): "; cin >> ch;

}while(ch != 'n' && ch != 'N');

return 0;

Rekurzív függvény

A Hanoi() függvényt a terv alapján kódoljuk. Az eredmény sztringet egy


ostringstream típusú objektumban állítjuk össze. Találkoztunk már ezzel a
cout-hoz hasonló adatfolyammal, amelybe betett adatok egy sztringbe
fűződnek fel, és ezt a sztringet tudjuk aztán az str() függvénnyel
lekérdezni.

string Hanoi(int n, int i, int j, int k)

ostringstream ss;

if(1 == n) ss << i << "->" << j;

else ss << Hanoi(n-1,i,k,j) << " , "

<< i << "->" << j << " , "

<< Hanoi(n-1,k,j,i);

return ss.str();

Tesztelés

Fekete doboz tesztesetek:

392
1. Hanoi(1,1,3,2)
2. Hanoi(2,1,3,2)
3. Hanoi(3,1,3,2)
4. Általános eset.
5. Skálázás. (Annak kimérése, hogy legfeljebb mekkora n értékekre
tudjuk kiszámolni az eredményt, és ez mennyire tart sokáig.)

Modul tesztek:
main()- A ciklikusan ismételhetőség kipróbálása.
ReadInt(),Nat()- Ezeket a függvényeket korábban már többször
használtuk, a tesztelésük ott megtalálható.
Hanoi()- A fekete doboz tesztesetek lefedik a tesztelést.

393
Teljes program

main.cpp:
#include <iostream>

#include <sstream>

#include "read.h"

using namespace std;

string Hanoi(int n, int i, int j, int k);

int main()

char ch;

do{

int n = ReadInt("n= ",

"Természetes számot kérek!\n",Nat);

cout << Hanoi(n,1,3,2) << endl;

cout << "Folytatja? (I/N): "; cin >> ch;

}while(ch != 'n' && ch != 'N');

return 0;

string Hanoi(int n, int i, int j, int k)

394
ostringstream ss;

if(1 == n) ss << i << "->" << j;

else ss << Hanoi(n-1,i,k,j) << " , "

<< i << "->" << j << " , "

<< Hanoi(n-1,k,j,i);

return ss.str();

read.h:
#ifndef READ_H

#define READ_H

#include <string>

bool ci(int k);

bool Nat(int n);

int ReadInt(std::string msg,

std::string errmsg, bool cond(int)= ci);

#endif

read.cpp:
#include "read.h"

#include <iostream>

using namespace std;

395
bool ci(int k){ return true;}

bool Nat(int n) { return n >= 0; }

int ReadInt(string msg,

string errmsg, bool cond(int) )

int n;

int hiba = true;

string tmp;

do{

cout << msg;

cin >> n;

if(cin.fail() || !cond(n)){

cout << errmsg << endl;

cin.clear();

getline(cin,tmp);

}while(hiba);

return n;

396
26. Feladat: Quick sort

Implementáljuk a gyorsrendezést (quick sort)!

Specifikáció

A = ( v : ℤk )
Ef = ( v=v’ )
Uf = ( v=rendezett(v’) )

Absztrakt program

Gyors rendezésnek (Quick sort) nevezik azt a módszert, amelyik a


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

397
a:= Divide(v,m,n)
x, a, f := v[n], m, n
a<f

v[a] ≤ x a<f
a := a+1
a<f
v[f], f := v[a], f-1 SKIP

v[f] ≥ x a<f
f := f-1
a<f
v[a], a := v[f], a+1 SKIP
v[a] := x

Ezt a Divide() függvényt hívja a gyorsrendezés rekurzív programja.


Quick(v,m,n)
m=n
a := Divide(v,m,n)
SKIP Quick(v,m,a-1)
Quick(v,a+1,n)

Szokás az elágazás feltételét úgy enyhíteni, hogy ha m és n eltérése


már elég kicsi, akkor egy másik fajta (pl. beillesztéses) rendezéssel rendezzük
a tömb kijelölt szakaszát. Ezáltal a rendezés még gyorsabb lesz.

Implementálás

398
A megoldó programot közvetlenül, rekurzívan hívható függvény segítségével
kódoljuk.

Komponens szerkezet

A rendezés algoritmusa a main.cpp állományban kerül kódolásra. Fontos


alprogramja a Quick() rekurzív eljárás, és az ebben hívott Divide()
függvény. A tömb szöveges állományból való feltöltését (Read() és a tömb
konzolablakba történő kiírását (Write()) külön csomagban (array.h-
array.cpp) helyezzük el.

main.cpp array.h - array.cpp

main() Read()
Quick()
Divide() Write()

10-5. ábra. Komponens szerkezet

Függvények hívási szerkezete

A vezérlést, azaz a komponensek egyes függvényeinek megfelelő sorrendben


történő meghívását a main függvény biztosítja.

Read()
main() Write()
Quick() Divide()
10-6. ábra. Alprogramok hívási láncai

A fő program

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

400
int main()

vector<int> v;

Read(v);

cout << "Rendezés előtt: \n";

Write(v);

Quick(v,0,(int)v.size()-1);

cout << "Rendezés után: \n";

Write(v);

return 0;

Rekurzív eljárás

A Quick() eljárást, és a Divide() függvényt a terv alapján kódoljuk.

void Quick(vector<int> &v, int m, int n)

if( m < n ){

int a = Divide(v,m,n);

Quick(v,m,a-1);

401
Quick(v,a+1,n);

int Divide(vector<int> &v, int a, int f)

int x = v[f];

while(a<f){

while( a<f && v[a]<=x ) ++a;

if( a<f ){ v[f] = v[a]; --f; }

while( a<f && v[f]>=x ) --f;

if( a<f ){ v[a] = v[f]; --a; }

v[a] = x;

return a;

402
Tömb műveletek

Egy egydimenziós tömbbel kapcsolatban két műveletet vezetünk be.


Az egyik művelet egy szöveges állományból tölt fel egy tömböt. Az
állomány első adata a tömb elemszámát, az azt követő adatai pedig a tömb
elemeit tartalmazza. Feltesszük, hogy ezek mind egész számok, és a tömb
hosszát megadó szám sem negatív.

void Read(vector<int> &t)

ifstream f("input.txt");

if (f.fail()) {

cout << "Hibás fájlnév!\n";

exit(1);

};

int n;

f >> n;

t.resize(n);

for (int i=0;i<n;++i){

f >> t[i];

A másik művelet egy tömb elemeit a konzolablakba írja ki.

403
void Write(const vector<int> &t)

for(int i=0; i<(int)t.size(); ++i) {

cout << "\t" << t[i];

cout << endl;

Tesztelés

Most csak a Quick() és a Divide() teszteléséhez elegendő fekete doboz


teszteseteket adjuk meg. A tesztelés többi részét az Olvasóra bízzuk.
1. Nulla hosszúságú tömb rendezése.
2. Egy elemű tömb rendezése.
3. Kettő-hatvány darab elemet tartalmazó tömb rendezése.
4. Kettő-hatványtól eltérő elemszámú tömb rendezése.
5. Csupa eltérő elemű tömb rendezése.
6. Csupa azonos elemű tömb rendezése.
7. Több azonos elemet is tartalmazó tömb rendezése.
8. Rendezett tömb rendezése.

404
Teljes program

main.cpp:
#include <iostream>

#include "array.h"

using namespace std;

void Quick(vector<int> &v, int m, int n);

int Divide(vector<int> &v, int m, int n);

int main()

vector<int> v;

Read(v);

cout << "Rendezés előtt: \n";

Write(v);

Quick(v,0,(int)v.size()-1);

cout << "Rendezés után: \n";

Write(v);

return 0;

405
}

void Quick(vector<int> &v, int m, int n)

if( m < n )

int a = Divide(v,m,n);

Quick(v,m,a-1);

Quick(v,a+1,n);

int Divide(vector<int> &v, int a, int f)

int x = v[f];

while(a<f){

while( a<f && v[a]<=x ) ++a;

if( a<f ){ v[f] = v[a]; --f; }

while( a<f && v[f]>=x ) --f;

if( a<f ){ v[a] = v[f]; --a; }

v[a] = x;

return a;

array.h:

406
#ifndef ARRAY_H

#define ARRAY_H

#include <vector>

void Read(std::vector<int> &t);

void Write(const std::vector<int> &t);

#endif

array.cpp:
#include "array.h"

#include <fstream>

#include <iostream>

using namespace std;

void Read(vector<int> &t)

ifstream f("input.txt");

if (f.fail()) {

cout << "Hibás fájlnév!\n";

exit(1);

};

407
int n;

f >> n;

t.resize(n);

for (int i=0;i<n;++i){

f >> t[i];

void Write(const vector<int> &t)

for(int i=0; i<(int)t.size(); ++i){

cout << "\t" << t[i];

cout << endl;

408
III. RÉSZ
PROGRAMOZÁS OSZTÁLYOKKAL

Az összetettebb feladatok megoldásához gyakran kell bevezetnünk olyan


adattípusokat, amelyek nem szerepelnek a választott programozási nyelv
típusai között. Az ilyen úgynevezett felhasználói típusok értékeinek
számítógépes ábrázolásáról, azaz reprezentálásáról, valamint a típus
műveleteinek implementálásáról ilyenkor magunknak kell gondoskodni.
A nevezetes szerkezetű típusok reprezentálásához a magas szintű
programozási nyelvek jól használható nyelvi elemeket biztosítanak. Például
C++ nyelven a rekord szerkezetű típusok a struct szerkezettel írhatóak le,
és az ilyen típusú változók (objektumok) komponenseire a mező nevek
(szelektorok) segítségével hivatkozhatunk. A típusműveleteket tehát ebben
az esetben a programozási nyelv szolgáltatja. Ugyanez a helyzet a tömbökkel
is. Jól használható C++ nyelvben az enum szerkezet az olyan típus értékeinek
felsorolására (értelemszerűen csak véges sok típusértékről lehet szó), amely
az értékek összehasonlításain kívül nem rendelkezik más típusművelettel. Az
alternatív szerkezetű típusokkal mostohán bánnak a programozási nyelvek. A
C++ nyelvbeli union szerkezet is csak első látásra tűnik alkalmas eszköznek
az alternatív szerkezetű típusok definiálására, de nem rendelkezik olyan
művelettel, amely egy ilyen típusú változóban éppen tárolt érték típusát
megmutatná.
A felhasználói típus definiálásának legelegánsabb eszköze az osztály
(class). Ezt akkor használjuk, ha az adattípus olyan műveletekkel
rendelkezik, amelyet nem biztosítanak közvetlenül a választott programozási
nyelv egyéb eszközei (és persze rendelkezik az adott nyelv az osztály definíció
lehetőségével, vagy ahhoz hasonló nyelvi elemmel). Egy osztály segítségével
tetszőleges szerkezetű típusértékek (objektumok) ábrázolhatók azáltal, hogy
egy típusérték komponenseinek értékei számára változókat (adattagok)
definiálhatunk, amelyekhez műveletek (metódusok) készíthetők.
Ebben a részben olyan feladatokkal foglalkozunk, ahol egy vagy több
olyan felhasználói típust kell a megoldáshoz megvalósítani, amelyek
leírásához osztályt használnunk. Az ilyen megoldásokban – a tervezésben és
a megvalósításban egyaránt – egy sajátos adattípus központú, típus-orientált
szemlélet uralkodik. Ahogy a hátra levő fejezetekben egy-egy feladat
megoldásában egyre több osztály kerül bevezetésre és ezek egymáshoz való
viszonya is egyre érdekesebbé válik, úgy jelennek meg fokozatosan
programjainkban az objektum-orientált programozási stílus jegyei is. Nincs
azonban arról szó, hogy a korábbi módszerek, tapasztalatok feleslegessé
válnának, hiszen például egy-egy típusművelet megvalósítása az eddigi
(hagyományos, procedurális) szemlélet mentén történik. A két programozási
paradigma egymást erősítve, kiegészítve jelenik meg, és a megoldandó
feladat sajátosságán múlik, hogy melyik dominál a megoldó programban. Az
illusztrációként választott feladatok megoldásának megtervezésekor az első
kötet típus központú szemléletére támaszkodó módszereket használjuk, mert
az kiváló alapot ad az objektum-orientált programozási stílus bevezetéséhez
és természetes módon köti össze azt a procedurális programozási stílussal.
Újdonsága miatt a következő néhány fejezetben nyilvánvalóan az 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.

410
11. A típus megvalósítás eszköze: az osztály

Programjaink tervezésekor két féle adattípussal találkozhatunk. Olyanokkal,


amelyeknek van a választott programozási nyelvben megfelelője, illetve
olyanokkal, amelyeknek nincs. Ez utóbbiakat magunknak kell definiálni. Az
objektum-orientált programozási nyelvek ehhez a definícióhoz hathatós
segítséget nyújtanak egy különleges nyelvi elem formájában, amelyet
osztálynak nevezünk.

Implementációs stratégia

Tekintsünk el egyelőre attól, hogy egy objektum-orientált programozási


nyelvben az osztály milyen sokrétű, kifinomult nyelvi lehetőségeket biztosít a
programozó számára, és helyette vezessünk be egy végletekig
leegyszerűsített osztály fogalmat. Az osztályra olyan egységként gondoljunk,
amely változókat és alprogramokat tartalmaz, ahol az alprogramokkal a
változók értékeit kérdezhetjük le vagy változtathatjuk meg, illetve a változók
értékétől függő tevékenységet hajthatunk végre. Az osztálynak van egy neve,
amellyel azonosítani tudjuk.
Egy osztály példányosítá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.

411
kizárólag az osztály leírásán belül, az osztály alprogramjaiban használhatjuk.
A metódus (tagfüggvény) egy osztályon belül definiált alprogram, amelyet az
osztály egy objektumával kapcsolatban lehet meghívni, és ezzel az
objektummal, illetve az objektum adattagjaival kapcsolatos tevékenységet
hajt végre.
Az osztály tökéletesen alkalmas eszköz arra, hogy nyelvi szinten leírjon
egy adattípust. Egy adat típusán az adat által felvehető típus-értékek
halmazát, az azokra megfogalmazott típus-műveleteket, típus-értékeinek
reprezentációját és a típus-műveleteket implementáló programokat értjük.
Ha ismert egy adattípus leírása, akkor ahhoz könnyen elkészíthető az azt
megvalósító osztály.

típus-értékek: A típus által definiált értékek.


típus-műveletek: A típus-értékekkel végzendő tevékenységek.
típus-reprezentáció: Egy típus-érték ábrázolására szolgáló érték-együttes
leírása.
típus-invariáns: Egy típus-értéket helyettesítő érték-együttesre előírt
feltétel.
típus-implementáció: A típus-műveletek tevékenységét elvégző program,
amely a típus-érték helyett annak reprezentációjával dolgozik.

11-1. ábra. Az adattípus leírásának részei

Az osztály neve a megvalósítandó típus neve lesz, adattagjai a típus


reprezentációjában bevezetett érték-együttes tagjait tartalmazó változók,
metódusai a típus-műveletek, azok törzsei pedig a típus-implementáció
programjai. Egy objektum egy típusérték tárolására szolgál.
Az osztályban deklarált adattagok a típus reprezentációjának leírására
szolgálnak. Ha egy típusértéket különféle adatokkal reprezentálunk, akkor
ezek az adatok egy-egy ilyen adattagban kerülnek elhelyezésre. Az

412
adattagokra sokszor fogalmazunk meg olyan megszorításokat, amelyek
együttesen a típus invariánsát teljesítik.
Az osztály egy metódusa felel meg a típus egy műveletének. A
metódusokat mindig az osztály egy objektumára hívjuk meg, azaz a
paraméterei között mindig szerepel legalább egy objektum. Tulajdonképpen
a metódusok egy objektummal kapcsolatos tevékenységeknek tekinthetők. A
helyesen megtervezett típus-műveletek kihasználják és megőrzik a művelet
által használt objektumok tagváltozóira vonatkozó típus-invariánst. Más
szavakkal, ha van egy olyan objektumunk, amely adattagjai kielégítik a tí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.

típus osztály
típus-értékek típus-műveletek class típus {

típus- típus-műveletek private:


reprezentáció
implementációi adattagok
típus-invariáns
public:

konstruktor

metódusok
11-2. ábra. Adattípust megvalósító osztály
}

metódusok definíciói
Egy típus osztályként való újrafogalmazáskor arra is kell ügyelni, hogy a
típus-invariáns már egy objektum létrehozásakor is fennálljon az objektum
adattagjaira. Erre az osztálynak egy speciális metódusa szolgál: a
konstruktor. Ez a metódus akkor hívódik meg, amikor létrehozunk egy új
objektumot, és ennek a metódusnak a törzse gondoskodhat a születő
objektum adattagjainak megfelelő, típus-invariáns szerinti kezdeti
értékadásáról.

413
Az osztályokat tartalmazó programok tesztelése is újabb módszereket
kíván. Egy-egy osztály önálló komponensként jelenik meg egy alkalmazásban,
ezért azt önmagában is tesztelni kell. teszteljük az eddig látott módon az
egyes metódusok működését, de tesztelni kell a metódusok tetszőleges
variációinak hatását is. Egy komponens teszteléséhez érdemes külön
tesztkörnyezetet, egy menüt biztosító keretprogramot készíteni, mert
többnyire az a főprogram, amelyen keresztül igénybe vesszük egy
komponens szolgáltatásait, nem képes teljes körűen letesztelni azt.

Nyelvi háttér

Az előző pontban bevezetett osztály tulajdonképpen nem nyelvi, hanem


tervezési eszköz, a típus szinonimája, amelynek azonban a nyelvi szinten
történő megfogalmazása sem bonyolult. Kell egy kulcsszó (mondjuk class),
amely bevezeti az osztályt leíró blokkot, ezután megadjuk az osztály egyedi
nevét, majd a leírást tartalmazó blokkban felsoroljuk az adattagokat és a
metódusokat.
Ebben és a következő fejezetben feltételezzük, hogy az adattagok
mindegyikéhez automatikusan rendelődik memóriafoglalás (eddig amúgy is
csak ilyen változókkal találkoztunk). Ha nem ilyen lenne, akkor az onnan
ismerhető fel, hogy az adattagok között úgynevezett pointer-változó jelenik
meg, amelyhez explicit módon (new utasítással) kell a saját kódunkban
(sokszor a konstruktorban) memóriafoglalást létrehoznunk. A pointer
adattagok jelenléte lényeges vízválasztó az osztályok kezelésében. Egyelőre
mi az egyszerű osztályokkal foglalkozunk, amelyek nem igényelnek explicit
módon végzett dinamikus helyfoglalást.
Az alábbiakban egy osztály C++ nyelvi megvalósítását láthatjuk.
// Osztály definíciója

class Tipus {

private:

// adattagok

Tipus11 tag1;

Tipus12 tag2;

414

public:

// konstruktorok

Tipus();

Tipus(…);

// destruktor

~Tipus();

// metódusok deklarációi

Tipus21 Metod1(…);

Tipus22 Metod2(…);

};

// Konstruktorok és a destruktor definíciói

Tipus::Tipus() {…}

Tipus::Tipus(…) {…}

Tipus::~Tipus(…) {…}

// Metódusok definíciói

Tipus21 Tipus::Metod1(…){…}

Tipus22 Tipus::Metod2(…){…}

415
Ha adott egy Tipus nevű osztályunk, akkor a segítségével (annak
mintájára) létrehozhatunk objektumokat, amelyekre változók segítségével
hivatkozhatunk. Az alábbi esetben a t változó azonosít majd egy objektumot.
Tipus t

Egy osztály sajátosan jelöli ki a benne definiált tagok láthatóságát.


Alapértelmezés szerint egy osztály minden tagja privát, azaz rájuk csak az
osztályon belül, azaz az osztályt definiáló blokkban és az osztályhoz tartozó
metódusok törzsében lehet hivatkozni. Ez utóbbit azért hangsúlyozzuk, mert
egy metódus definíciója, azaz a metódus törzse nem feltétlenül az osztály
blokkján belül helyezkedik el. (Megjegyezzük, hogy ezzel enyhül a szokásos
láthatósági szabály, mely szerint egy azonosító láthatósága arra blokkra
terjed ki, amelyikben deklarálták.) A privát tagokat az egyértelműség
kedvéért private-ként jelöljük meg akkor is, ha ez a tulajdonság
alapértelmezett.
Ha azt szeretnék biztosítani, hogy egy tagváltozó vagy különösen egy
metódus az osztályon kívül is látható legyen, akkor ezt a public kulcsszó
segítségével jelölhetjük ki. A publikus tagok láthatósága kiterjed minden
olyan helyre, ahol maga az osztály látható. Egy objektum adattagjait
általában privátként szokták deklarálni, de ha mégis publikussá tesszük,
akkor a
t.tag1

formában lehetne rájuk hivatkozni. Ha viszont az elérhetősége privát marad,


akkor közvetlenül nem hivatkozhatunk rá az osztályon kívül, a fenti
kifejezésre fordítási hibát kapunk.
Ha egy metódust az osztály leíráson kívül akarjuk meghívni az osztály
egy objektumára, akkor a metódus láthatósága publikus kell legyen. Egy
metódus abban különbözik a szokásos alprogramtól, hogy csak az adott
osztály egy objektumával (ez a hívó objektum) lehet meghívni. A hívó
objektumnak a metódus neve előtt kell a hívásban szerepelnie:
t.Metod1(…)

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

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

private:

int n;

std::string str;

public:

Tipus(){n = 0; str = "hello";}

Tipus(int a, std::string b)

{ n = i; str = b; … }

};

418
Objektum létrehozásakor mindig az a konstruktor hajtódik végre,
amelyiknek formális paraméterlistája illeszkedik a létrehozásnál feltüntetett
aktuális paraméterlistához. A Tipus o forma alkalmazásakor az üres
paraméterlistájú konstruktor hívódik meg, a Tipus o(12, "hello")
esetén pedig az aktuális paraméterlista elemeinek száma, típusa és sorrendje
alapján beazonosítható konstruktor.
Az adattagok kezdeti értékadása az alábbi konstruktor segítségével is
elvégezhető. Ennek hatása egyenértékű a fenti mutatott osztály második
konstruktorával.
Tipus(int a, std::string b):n(i),str(b) { … }

A konstruktor feladata egyszerű osztály esetén az, hogy a létrehozandó


objektum adattagjaihoz a típus-invariánst kielégítő kezdőértéket rendelje.
A destruktor szerepe egyszerű osztályoknál az egyes adattagok
megfelelő lezárása, amennyiben ehhez nem elegendő az adattagok
destruktora, amelyek viszont automatikusan meghívódnak. Ennél fogva
egyszerű osztályoknál gyakori, hogy nem írunk destruktort.
Egy osztálynak az adattagokon és metódusokon kívül lehetnék még
egyéb tagjai is, mint konstansok, típusdefiníciók, belső osztályok stb.
Két különböző osztálynak lehet ugyanolyan nevű és típusú adattagja,
ugyanolyan nevű és típusú metódusa. Sőt egy osztályon belül lehetnek
azonos nevű metódusok is, ha azoknak eltér a típusa: paraméterlistája,
visszatérési érték típusa és attribútumai (private/public, static,
const). Ezt a többalakúságot az operátorokra is alkalmazhatjuk, azaz
például új, az adott osztály objektumaihoz köthető jelentés adható például a
+ operátornak. (Erre majd mutatunk példákat.)
Az osztály leírást a kódban kétféleképpen helyezhetjük el. Az első az,
amikor az osztályt használó forrásállomány elején definiáljuk az osztályt, és
ugyanebben a forrásállományban az egyéb függvény-definíciók között
definiáljuk az osztály metódusait. A másik az, amikor az osztály leírást külön
csomagba helyezzük. C++ nyelven az osztály-definíciót egy fejállományba, az
ahhoz tartozó metódus-definíciókat pedig az ahhoz tartozó forrásállományba
szoktuk tenni, és a fejállományt „beinklúdoljuk” mind a metódus-definíciókat

419
tartalmazó forrásállományba, mind az összes olyan forrásállományba, ahol az
osztály szolgáltatásait használni akarjuk. Az összetartozó fejállomá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ű.

420
Osztállyal kapcsolatos speciális nyelvi fogalmak

Adattagok kezdeti értékadása


A konstruktorok definíciójában a formális paraméter listát követő kettőspont
után az adattagok kezdeti értékadásait hívhatjuk meg. Ehhez az érintett
tagváltozókat kell felsorolni úgy, hogy a nevük mögötti zárójelben adjuk meg
a nekik szánt kezdő értéket:
Tipus::Tipus(): tag1(…), tag2(…), … {…}
Értékadás operátor, másoló konstruktor
Minden osztály alapértelmezés szerint rendelkezik egy úgynevezett másoló
konstruktorral (amely egy már létező objektum adattagjainak értékeivel hoz
létre egy új objektumot) és egy értékadás operátorral (amely egy objektum
adattagjaival felülírja egy másik objektum adattagjait).
Tipus o2(o1); // 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

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

422
27. Feladat: UFO-k

Képzeljük el, hogy egy űrállomást állítanak Föld körüli pályára, amelynek az a
feladata, hogy adott számú észlelt, de ismeretlen tárgy közül megszámolja,
hány tartózkodik az űrállomás közelében (mondjuk 10000 km-nél közelebb
hozzá).

Specifikáció

A feladat részletes elemzését már megtettük (I. kötet 7.1 példa), most csak
annak eredményét idézzük fel.

A = ( g : Gömb, v : Pontn, db : ℕ )
Ef = ( g=g' v=v' )
1.
n
Uf = ( Ef db 1 )
i 1
v[i ] g
Gömb típus:
gömb l:=p g
g:Gömb, p:Pont, l:

(c,r):Pont ℝ, l:=távolság(c,p) r
r 0 p:Pont, d:ℝ, l:

Pont típus:
pont d:=távolság(p,q)
p,q:Pont, d:ℝ

(x,y,z): ℝ ℝ ℝ
d: ( p.x q.x) 2 ( p. y q. y ) 2 ( p.z q.z ) 2

Absztrakt program

423
Ezt a feladatot legfelső szinten egy számlálás oldja meg.

db := 0
i =1 .. n

v[i] g
db := db + 1 SKIP

Implementálás

Az implementáció magán viseli mind a funkció vezérelt, mind a típus


központú szemléletmódot. Mindezzel együtt ez már egy objektum orientált
kódot eredményez.

Komponens szerkezet

A megoldó program kódját négy részre vágjuk. Külön csomagot alkot a Gömb
típust (Sphere), külön a Pont típust (Point) megvalósító kód, külön
csomagba kerülnek az egész és valós számok beolvasását segítő függvények
(a ReadInt() és ReadReal() függvényekkel már a korábbi fejezetekben
találkoztunk), a főcsomag pedig a main függvényt tartalmazza. A sphere.h
és point.h csomagok kialakítása a típus orientáltság, a read.h-read.cpp
pedig a funkció orientáltság jegyében történt.

main.cpp sphere.h point.h read.h - read.cpp

Sphere Point ReadInt()

• main() • Sphere() • Point() ReadReal()


• In() • Set()
• Distance() Pos()

11-3. ábra. Komponens szerkezet

424
425
Függvények hívási láncolata

A vezérlést, azaz a komponensek egyes függvényeinek megfelelő sorrendben


történő meghívását a main függvény biztosítja. Minden hívás vagy egy gömb
(Sphere), vagy egy pont (Point) objektummal kapcsolatos tevékenységért
felelős.

ReadReal() Pos()

ReadInt()

Point()
main()
Set()

Sphere()

In() Distance()

11-4. ábra. Alprogramok hívási lánca

Gömb típus megvalósítása

class Sphere{

private:

Point c;

double r;

public:

enum Errors { Negativ_Radius };

Sphere(const Point &p, double a) {

if (a<0) throw Negativ_Radius;

c=p; r=a;

426
}

bool In(const Point &p)

{ return c.Distance(p)<=r; }

};

A Gömb típust a Sphere osztály valósítja meg. Tekintettel a


metódusok egyszerű voltára, azokat inline módon, a definícióba ágyazva
implementáltuk, ezért nem tartozik a sphere.h állományhoz sphere.cpp
állomány is.
A konstruktor negatív sugár esetén kivételt dob. Az itt implementált
programban ez a kivétel ugyan nem következhet be, mert majd, mint látni
fogjuk, a sugár értékének beolvasásánál ellenőrzést végzünk.
Az In() metódus a Point osztály Distance() metódusát hívja,
annak segítségével vizsgálja meg, hogy a megadott pont mennyire esik közel
a gömb középpontjához.

Pont típus megvalósítása

A Pont típust a Point osztály valósítja meg. A metódusokat itt is inline


módon implementáljuk.

class Point{

private:

double x,y,z;

public:

Point(){ x = y = z = 0.0;}

void Set(double a, double b, double c)

{ x = a; y = b; z = c;}

double Distance(const Point &p) const {

427
return sqrt(pow(p.x-x,2)+ pow(p.y-y,2)

+ pow(p.z-z,2));

};

A Point() konstruktor egy origóba pozícionált pontot hoz létre. Egy


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

Főprogram kódolása

428
A main függvény először az űrállomás (a gömb) középpontjának koordinátáit
olvassa be (ReadReal()), létrehozza a középpontot (Point()) és beállítja
(Set()) a koordinátáit, majd a sugár beolvasása után magát a gömböt is
megalkotja (Sphere()).

double x,y,z,r;

cout << "Az űrállomás koordinátái:\n";

x = ReadReal("\t x: ", "Valós számot várok!");

y = ReadReal("\t y: ", "Valós számot várok!");

z = ReadReal("\t z: ", "Valós számot várok!");

Point c; c.Set(x,y,z);

r = ReadReal("Űrállomás körzetének sugara: ",

"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!");

429
vector<Point> v(n);

for(int i=0; i<n; ++i){

cout << "Az " << i+1

<< "-dik UFO koordinátái:\n";

x = ReadReal("\tx: ","Valós számot várok!");

y = ReadReal("\ty: ","Valós számot várok!");

z = ReadReal("\tz: ","Valós számot várok!");

v[i].Set(x,y,z);

Az absztrakt főprogram egy egyszerű számlálás, amely a „benne van-e


egy pont a gömbben” műveletét hívja meg (In()). Ezt az eredmény kiírása
követi.

int db = 0;

for(int i=0; i<n; ++i){

if(g.In(v[i])) ++db;

cout << "Közeli UFO-k száma: " << db;

430
Tesztelés

Fekete doboz tesztesetek: (a számlálás, annak intervalluma és különleges


értékek tesztelése)
1. Nulla darab pont esete.
2. Nulla sugarú gömb esete benne levő ponttal.
3. Olyan gömb és pontok, ahol az első pont/utolsó pont esik csak a
gömbbe.
4. Olyan adatok, hogy egyetlen pont sem esik a gömbbe.
5. Olyan adatok, hogy minden pont a gömbbe esik.
6. Általános eset.
7. Negatív sugarú gömb.

A komponensek (osztályok) tesztelése nem túl bonyolult, hiszen a


konstruktoron és értékadó (setter) metóduson kívül mindkét osztály egyetlen
metódust tartalmaz csak, így a metódusok variációinak kipróbálására nincs
szükség.
1. Sphere osztály tesztelése
a. A Negativ_Radius kivétel dobásának tesztelése.
b. Nulla sugarú gömb létrehozása.
c. Egy gömb és egy a gömbbe eső pont vizsgálata.
d. Egy gömb és egy a gömbön kívüli pont vizsgálata.
e. Egy gömb és egy a gömb felületére eső pont vizsgálata.
2. Point osztály tesztelése.
a. Pont létrehozása.
b. Pont pozíciójának megváltoztatása
c. Két pont távolsága (azonos, csak egy koordinátában eltérő,
csak két koordinátában eltérő, mindhárom koordinátában
eltérő pontokkal)
d. A p és q pontok, illetve q és p pontok távolsága megegyezik-e
(szimmetria)?

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

432
Teljes program

main.cpp:
#include <iostream>

#include <vector>

#include "read.h"

#include "point.h"

#include "sphere.h"

using namespace std;

int main()

// Űrállomás beolvasása

double x,y,z,r;

cout << "Az űrállomás koordinátái:\n";

x = ReadReal("\t x: ", "Valós számot várok!");

y = ReadReal("\t y: ", "Valós számot várok!");

z = ReadReal("\t z: ", "Valós számot várok!");

Point c; c.Set(x,y,z);

r = ReadReal("Űrállomás körzetének sugara: ",

"Nem-negatív valós számot várok!", Pos);

Sphere g(c,r);

// UFO-k beolvasása

int n = ReadInt("UFO-kszáma: ",

433
"Természetes számot várok!");

vector<Point> v(n);

for(int i=0; i<n; ++i){

cout << "Az " << i+1

<< "-dik UFO koordinátái:\n";

x = ReadReal("\tx: ","Valós számot várok!");

y = ReadReal("\ty: ","Valós számot várok!");

z = ReadReal("\tz: ","Valós számot várok!");

v[i].Set(x,y,z);

// Számlálás

int db = 0;

for(int i=0; i<n; ++i){

if(g.In(v[i])) ++db;

// Kiírás

cout << "Közeli UFO-k száma: " << db;

char ch; cin>>ch;

return 0;

sphere.h:
#ifndef SPHERE_H

#define SPHERE_H

434
#include "point.h"

class Sphere{

private:

Point c;

double r;

public:

enum Errors { Negativ_Radius };

Sphere(const Point &p, double a) {

if (a<0) throw Negativ_Radius; c=p; r=a;

bool In(const Point &p)

{ return c.Distance(p)<=r; }

};

#endif

point.h:

#ifndef SPHERE_H

#define SPHERE_H

435
#include <cmath>

class Point{

private:

double x,y,z;

public:

Point(){ x = y = z = 0.0;}

void Set(double a, double b, double c)

{ x = a; y = b; z = c;}

double Distance(const Point &p) const {

return sqrt(pow(p.x-x,2)+ pow(p.y-y,2)

+ pow(p.z-z,2));

};

#endif

436
read.h:

#ifndef _READ_H

#define _READ_H

#include <string>

bool All(int n){ return true; }

bool All(double r){ return true; }

bool Pos(double a){ return a>=0.0;}

int ReadInt (std::string msg, std::string errormsg,

bool cond(int) = All);

double ReadReal(std::string msg,

std::string errormsg,

bool cond(double) = All);

#endif

read.cpp:

#include "read.h"

#include <iostream>

using namespace std;

int ReadInt(string msg, string errmsg,

bool check(int) )

int n;

int error = true;

do{

cout << msg; cin >> n;

437
if(error = cin.fail() || !check(n)){

cout << errmsg << endl;

cin.clear();

string tmp; getline(cin,tmp);

}while(error);

return n;

double ReadReal(string msg, string errmsg,

bool check(double))

double a;

bool error = true;

do{

cout << msg; cin >> a;

if(error = cin.fail() || !check(n)){

cout << errmsg << endl;

cin.clear();

string tmp; getline(cin,tmp);

}while(error);

return a;

438
28. Feladat: Zsák

Adott egy n, de legalább 1 hosszúságú legfeljebb 0 és 99 közé eső egész


számokat tartalmazó tömb. Melyik a tömbnek a leggyakrabban előforduló
eleme!

Specifikáció

A feladat részletes elemzésével már találkoztunk (I. kötet 7.2 példa), most
csak annak eredményét idézzük fel.

A = ( t : {0..99} n, b : Zsák, e : {0..99} )


Ef = ( t = t’ n≥1 )
n
Uf = ( Ef b {t[i]} e = MAX(b) )
i 1

Zsák típus:
zsák b:= b:Zsák
b:= b {e} e:{0..99}
e:=MAX(b)

v: e [0..99]: v[e]:=0
v[e]:=v[e]+1 e:{0..99}

Absztrakt program

A feladatot egy olyan összegzés oldja meg, ahol az összeadás


műveletét a zsákunió művelete helyettesíti. Ezt a zsák maximális előfordulás
számú elemének kiválasztása követi. A MAX(b) értelmes, mivel a feladat
előfeltétele garantálja, hogy legalább egy elem be fog kerülni a zsákba.

439
b :=
i =1..n

b := b {t[i]}
e := MAX(b)

Implementálás

Az implementálás egy objektum orientált kódot eredményez.

Komponens szerkezet

A megoldásban külön csomagba kerül a Zsák típus (Bag) és külön csomagba a


főprogram, amely a main függvény mellett a feladatban szereplő bemeneti
tömb feltöltését végző Read() függvényt is tartalmazza.

main.cpp bag.h - bag.cpp

Bag
• main() • Bag()
• Read() • Put()
• Max()
11-5. ábra. Komponens szerkezet

Függvények hívási láncolata

A vezérlést, azaz a komponensek egyes függvényeinek megfelelő sorrendben


történő meghívását a main függvény biztosítja.

440
Read()

Bag()
main()
Put()

Max()

11-6. ábra. Alprogramok hívási lánca

Zsák típus megvalósítása

A Zsák típust megvalósító Bag osztályt a bag.h állományban definiáljuk. Ez a


tervben bevezetett három metódus mellett két kivételt is dobhat. A
WrongInput kivétel csak a Put()metódusban keletkezhet, az EmptyBag
csak a Max() metódusban.

class Bag{

public:

enum Errors{WrongInput, EmptyBag};

Bag();

void Put(int e);

int Max() const;

private:

static const int n = 100;

int v[n];

};

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

#include "bag.h"

using namespace std;

Bag::Bag() { for (int k=0; k<n; ++k) v[k] = 0; }

void Bag::Put(int e)

if (e<0 || e>n-1) throw WrongInput;

++v[e];

int Bag::Max() const

int max = v[0];

int e = 0;

for (int k=1; k<n; ++k)

442
if (v[k]> max){ max = v[k]; e = k; }

if (0 == max) throw EmptyBag;

return e;

Főprogram kódolása

A main függvény először deklarálja a t bementi tömböt és létrehozza a


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

vector<int> t;

Read(t);

Bag b;

for(int i=0;i<(int)t.size();++i){

try{ b.Put(t[i]); }

443
catch(Bag::Errors ex){

if(Bag::WrongInput == ex)

cout << "Hibás adat a tömbben!\n";

try{ cout << "Leggyakoribb elem: " << b.Max();}

catch(Bag::Errors ex){

if(Bag::EmptyBag == ex)

cout << "Üres tömb!\n";

444
Tesztelés

Fekete doboz tesztesetek:


Érvénytelen esetek:
1. Üres bemeneti tömb
2. Egyetlen, nem 0 és 99 közötti számot tartalmazó bementi tömb.
3. Több, nem 0 és 99 közötti számot tartalmazó bementi tömb.
4. Csupa, nem 0 és 99 közötti számot tartalmazó bementi tömb.
Érvényes esetek:
1. Egyetlen 0 és 99 közötti számot tartalmazó bementi tömb.
2. Olyan bemeneti tömb, amely első/utolsó eleme a nulla, ezen kívül
még egy nullát tartalmaz, minden más számból legfeljebb egyet.
3. Olyan bemeneti tömb, amely első/utolsó eleme a 99, ezen kívül még
egy 99-t tartalmaz, minden más számból legfeljebb egyet.
4. Olyan bemeneti tömb, amelyben több szám is egyforma
gyakorisággal található.

A főprogram fehérdoboz tesztelése sem igényel újabb teszteseteket, a


bemeneti tömb feltöltése, az elemek zsákba pakolása a fekete doboz
tesztesetekkel már ki lett próbálva.

Komponens teszt. Külön tesztelendő a Bag osztály. Elsősorban a Max()


művelet tesztelésére kell figyelnünk (amikor a zsák első vagy utolsó eleme a
legnagyobb számosságú, vagy több egyformán maximális számosság is van),
de ezt a fenti érvényes esetek lefedik. Ezen kívül a kivétel dobásokat és azok
kezelését kell még ellenőrizni. A metódusok variációinak tesztje itt nem kell.

445
Teljes program

main.cpp:
#include <iostream>

#include <fstream>

#include <vector>

#include <cstdlib>

#include "bag.h"

using namespace std;

void Read(vector<int> &t);

int main()

vector<int> t;

Read(t);

Bag b;

for(int i=0;i<(int)t.size();++i){

try{ b.Put(t[i]); }

catch(Bag::Errors ex){

if(Bag::WrongInput == ex)

cout << "Hibás adat a tömbben!\n";

446
try{ cout << "Leggyakoribb elem: " << b.Max();}

catch(Bag::Errors ex){

if(Bag::EmptyBag == ex)

cout << "Üres tömb!\n";

char ch; cin >> ch;

return 0;

void Read(vector<int> &t)

ifstream f("input.txt");

if (f.fail()) {

cout << "Hibás fájlnév!\n";

exit(1);

};

int n;

f >> n;

t.resize(n);

for (int i=0;i<n;++i){

f >> t[i];

bag.h:
#ifndef BAG_H

447
#define BAG_H

#include <vector>

class Bag{

public:

enum Errors{WrongInput, EmptyBag};

Bag();

void Put(int e);

int Max() const;

private:

static const int n = 100;

int v[n];

};

#endif

bag.cpp:

#include "bag.h"

using namespace std;

448
Bag::Bag()

for (int k=0; k<n; ++k) v[k] = 0;

void Bag::Put(int e)

if (e<0 || e>n-1) throw WrongInput;

++v[e];

int Bag::Max() const

int max = v[0];

int e = 0;

for (int k=1; k<n; ++k)

if (v[k]> max){ max = v[k]; e = k; }

if (0 == max) throw EmptyBag;

return e;

449
29. Feladat: Síkvektorok

Adott n+1 darab síkvektor. Igaz-e, hogy az első n darab síkvektor összege
merőleges az n+1-edik síkvektorra?

Specifikáció

A feladat megoldásához ki kell számolnunk az n darab síkvektor


eredőjét (összegzés), majd ennek az n+1-edik vektorral vett skaláris
szorzatát. Ha ez nulla, akkor merőleges az eredő az n+1-edik síkvektorra.

A = ( t : Síkvektorn, v : Síkvektor , l : )
Ef = ( t=t' v=v' )
n
Uf = ( Ef s 1 l = (s*v=0.0) )
i 1
A megoldáshoz definiálnunk kell a síkvektorok típusát. A síkvektorokat
origóból induló helyvektorokként ábrázoljuk, és a végpontjuk koordinátáival
reprezentáljuk. Öt műveletet vezetünk be: a nullvektor létrehozását, egy
vektor végpontjának egyik illetve másik koordinátájának megváltoztatását,
egy vektorhoz egy másik vektor hozzáadását és két vektor skaláris szorzását.

Síkvektor típus:
síkvektor v := nullvektor v:Síkvektor
v.SetX(a), v.SetYb() 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: ℝ

450
Absztrakt program

A feladatot egy összegzés, majd a skaláris szorzás eredményének


vizsgálata oldja meg.

s := nullvektor
i =1 .. n
s := s + t[i]
l := s*v=0.0

Implementálás

Az implementálás során elkészítjük a Síkvektor típust, majd kódoljuk a fenti


absztrakt programot.

Komponens szerkezet

A megoldó program kódját három részre vágjuk. Külön csomagot alkot a


Síkvektor típusát megvalósító Vector2D osztály, külön csomagba kerülnek
az egész, a természetes és a valós számok beolvasását segítő függvények (a
ReadInt() és ReadReal() függvények), a főprogram pedig a main
függvényt tartalmazza.
A Vector2D osztály operator+=() metódusa a v := v + 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.

451
vector2d.h -
main.cpp read.h - read.cpp
vector2d.cpp

main() Vector2D ReadInt()


• Vector2D()
Fill() • Vector2D(a,b) Nat()
• SetX(),SetY()
• operator+=()
Sum() • operator*() ReadReal()

11-7. ábra. Komponens szerkezet

Függvények hívási láncolata

A vezérlést, azaz a komponensek egyes függvényeinek megfelelő sorrendben


történő meghívását a main függvény biztosítja.

ReadInt() Nat()
Vector2D()
ReadReal()
Fill()
SetX(),
SetY()

main() Vector2D()
Sum()
operator+=()
ReadReal()

Vector2D(,)

operator*()

11-8. ábra. Alprogramok hívási lánca

452
Főprogram kódolása

A main függvény először a síkvektorok tömbjét hozza létre, amelyet a


Fill() függvény segítségével tölt fel, majd a Sum() függvény segítségével
kiszámolja a síkvektorok összegét. A külön síkvektor beolvasása illetve
létrehozása után pedig ennek és az összegnek a skaláris szorzatát számolja ki,
és az eredményt összeveti a 0.0-val.

cout << "Összeadandó vektorok:" << endl;

vector<Vector2D> t;

Fill(t);

Vector2D s = Sum(t);

cout << "Külön vektor:" << endl;

Vector2D v( ReadReal("x = "), ReadReal("y = "));

if(s*v == 0.0) cout << "Merőleges";

else cout << "Nem merőleges.";

A Fill() eljárás megadott méretre módosítja a paraméterként


kapott tömböt, majd – mivel ebben csupa nullvektor van – a felhasználó
adatai alapján módosítja a tömb összes síkvektorának koordinátáit. A
ReadReal() alprogram olyan megvalósítását igényli az alábbi kód, amelyik
második és harmadik paramétere is rendelkezik alapértelmezett értékkel, így
hívásakor elég csak az első paraméterét megadni.

void Fill(vector<Vector2D> &t)

453
{

t.resize(ReadInt("Hány vektort fogsz megadni? ",

"Természetes számot kérek!", Nat()));

for(int i=0; i<(int)t.size(); ++i){

t[i].SetX(ReadReal("x = "));

t[i].SetY(ReadReal("y = "));

A Sum() függvény a tömb síkvektorait összegzi.

Vector2D Sum(const vector<Vector2D> &t)

Vector2D s;

for(int i=0; i<(int)t.size(); ++i){

s+=t[i];

return s;

Síkvektor típus megvalósítása

A Síkvektor típust a vector2d.h-ban, a hozzáadás és skaláris szorzás


metódusait a vector2d.cpp-ben definiáljuk.

class Vector2D{

454
private:

double x,y;

public:

Vector2D():x(0),y(0){}

Vector2D(double a, double b):x(a),y(b){}

void SetX(double r){ x = r;}

void SetY(double r){ y = r;}

Vector2D operator+=(const Vector2D &v2);

friend double operator*(const Vector2D &v1,

const Vector2D &v2);

};

Egy vektorhoz egy másikat hozzáadó metódust definiálhattuk volna a


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

455
Hasonlóan „szimmetrikus” megoldást kínál a friend double
Scalar(const Vector2D &v1, const Vector2D &v2) alkalmazása.
Ez ugyan nem az osztály metódusa, de a friend tulajdonság miatt ugyanúgy
hivatkozhat a törzsében az osztály privát adattagjaira, mint a metódusok,
ennél fogva az osztály külső metódusának tekinthető. Nem hívó
objektummal aktiváljuk, hanem a d = Scalar(v1,v2) hívással, de a
paraméterek között szerepel legalább egy (itt kető) vektor típusú objektum.
Ugyanezt operátorként bevezetve a friend double operator*(const
Vector2D &v1, const Vector2D &v2) is biztosítja, amely a d =
v1*v2 utasítással hívható. Annak érdekében, hogy a megoldásunkban legyen
operátor felüldefiniálás és barát függvény is, ez utóbbi változatot használjuk.

Vector2D Vector2D::operator+=(const Vector2D &v)

x+=v.x; y+=v.y;

return *this;

double operator*( const Vector2D& v1,

const Vector2D &v2)

return v1.x*v2.x + v1.y*v2.y;

Tesztelés

A Vector2D komponens teszteje:

A teszteléshez célszerű az osztályban egy olyan metódust is készíteni,


amelyik meg tud jeleníteni egy síkvektort (kiírja a koordinátáit). Ezek után
készítünk egy tesztprogramot (egy menüt), amely tetszőleges sorrendben
hívhatja az osztály metódusait.

456
1. Konstruktor teszt. (Létrejön-e a nullvektor?)

2. SetX() és SetY() tesztje. (Megváltozik egy síkvektor koordinátája?)

3. Hozzáadás műveletének tesztje. (Egy vektorhoz hozzáadni a nullvektort,


az egységvektorokat, egy tetszőleges vektort.)

4. Skalárszorzás tesztje. (nullvektorok szorzása, nem nullvektorok szorzása,


kommutatívitás)

Variációs teszt (a metódusok különféle sorrendben történő kipróbálása) itt


nem kell.

Fekete doboz tesztesetek: A Sum()-beli összegzésre és a vizsgált vektor, a


tömbbeli vektorok eredőjének merőlegességre vonatkozó tesztek, illetve a
különleges adatok (nulla, egy, negatív számok, törtek).

1. A vizsgált vektor nullvektor (0.0, 0.0) és üres a vektortömb. Válasz:


merőleges.

2. A vizsgált vektor nullvektor és nem üres vektortömb. Válasz: merőleges.

3. A vizsgált vektor nem nullvektor és a vektortömb eredője nullvektor


([(0.0, 0.0)] vagy [(3.0 ,3.0), (-3.0,-3.0)]). Válasz: merőleges.

4. A vizsgált vektor (3.0, 3.0) és a vektortömb [(-3.0, 3.0)] . Válasz:


merőleges.

5. A vizsgált vektor (3.0, 3.0) és a vektortömb [(-3.5, -3.71)] . Válasz: nem


merőleges.

6. A vektortömb első/utolsó eleme nullvektor, de a vizsgált síkvektor az


eredőre nem merőleges.

457
Fehér doboz tesztesetek:

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

458
Teljes program

main.cpp:
#include <iostream>

#include <string>

#include <vector>

#include "vector2D.h"

#include "read.h"

using namespace std;

void Fill(vector<Vector2D> &t);

Vector2D Sum(const vector<Vector2D> &t);

int main()

cout << "Összeadandó vektorok:" << endl;

vector<Vector2D> t;

Fill(t);

Vector2D s = Sum(t);

cout << "Külön vektor:" << endl;

Vector2D v( ReadReal("x = ",""),

ReadReal("y = ",""));

if(s*v == 0.0) cout << "Merőleges";

459
else cout << "Nem merőleges.";

char ch; cin >> ch;

return 0;

void Fill(vector<Vector2D> &t)

t.resize(ReadInt("Hány vektort fogsz megadni? ",

"Természetes számot kérek!", Nat));

for(int i=0; i<(int)t.size(); ++i){

t[i].SetX(ReadReal("x = ",""));

t[i].SetY(ReadReal("y = ",""));

Vector2D Sum(const vector<Vector2D> &t)

Vector2D s;

for(int i=0; i<(int)t.size(); ++i){

s+=t[i];

return s;

vector2d.h:
#ifndef VECTOR2D_H

#define VECTOR2D_H

460
class Vector2D{

private:

double x,y;

public:

Vector2D():x(0),y(0){}

Vector2D(double a, double b):x(a),y(b){}

void SetX(double r){ x = r;}

void SetY(double r){ y = r;}

Vector2D operator+=(const Vector2D &v2);

friend double operator*(const Vector2D &v1,

const Vector2D &v2);

};

#endif

vector2d.cpp:

#include "vector2D.h"

using namespace std;

Vector2D Vector2D::operator+=(const Vector2D &v)

x+=v.x; y+=v.y;

return *this;

461
double operator*( const Vector2D& v1,

const Vector2D &v2)

return v1.x*v2.x + v1.y*v2.y;

read.h:

#ifndef READ_H

#define READ_H

#include <string>

bool ci(int k);

bool cd(double k);

bool Nat(int n);

int ReadInt(std::string msg,

std::string errmsg = "",

bool check(int) = ci);

double ReadReal(std::string msg,

std::string errmsg = "",

bool check(double) = cd);

#endif

read.cpp:

#include "read.h"

462
#include <iostream>

using namespace std;

bool ci(int k){ return true; }

bool cd(double k){ return true; }

bool Nat(int n) { return n >= 0; }

int ReadInt(string msg, string errmsg,

bool check(int) )

int n;

int error = true; string tmp;

do{

cout << msg; cin >> n;

if(cin.fail() || !check(n)){

cout << errmsg << endl;

cin.clear();

getline(cin,tmp);

}while(error);

return n;

double ReadReal(string msg, string errmsg,

463
bool check(double))

double a;

bool error = true; string str;

do{

cout << msg; cin >> str;

a = atof(str.c_str());

error = 0 == a && str != "0";

if(error) cout<< errmsg<< endl;

}while(error);

return a;

464
C++ kislexikon

osztály class{

private:

int i;

static const int n = 3;

public:

Tipus();

Tipus(…);

~Tipus();

void method1(…);

void method2(…) const;

void method3(…){ … }

Tipus operator+=(const Tipus &v);

friend void method4(…);

};

Tipus::Tipus(){…}

Tipus::Tipus(int a):a(i){…}

Tipus::~Tipus(){ … }

void Tipus::method1(…){…}

void Tipus::method2(…) const {…}

Tipus Tipus::operator+=(const Tipus &p){…}

void method4(…){…}

465
láthatóság private , public

konstruktor Tipus();

Tipus(…);

kezdeti Tipus::Tipus(int a):a(i){ … }

értékadás
destruktor ~Tipus();

konstans tag static const int n = 3;

hívás t.method(…);

konstans void method2(…) const;

metódus
inline void method3(…) { … }

operátor Tipus operator+=(const Tipus &p);

túlterhelés // hívása: t += t1;

barát friend void method4(…);

466
12. Felsorolók típusainak megvalósítása

Könyvünk első kötetében számos feladat megoldását terveztük a felsorolókra


általánosított programozási tételekre történő visszavezetés segítségével. Az
ilyen megoldások implementálásában az egyedüli problémát az alkalmazott
felsoroló megvalósítása jelentheti. Néha a felsoroló típusa meglévő típusok
valamelyikével kiváltható, máskor azonban egy saját osztályt kell ehhez
definiálni.

Implementációs stratégia

A felsoroló egy olyan objektum, amely bizonyos elemek felsorolását teszi


lehetővé. A felsorolandó elemek elhelyezkedhetnek közvetlenül egy
gyűjteményben (tárolóban), és ilyenkor a felsorolónak nem kell mást tennie,
mint bejárni a tárolt elemeket. Máskor viszont a felsorolni kívánt elemek
nem állnak explicit módon rendelkezésünkre, azokat elő kell állítani, ki kell
számolni. Igaz, hogy a felsoroló használata ilyenkor is azt az illúziót kelti,
mintha az általa felsorolt elemek sorozata valóban létezne, de ilyenkor a
felsoroló a valóságtól elvonatkoztatott, absztrakt objektum.

t.First()

t.End()
feldolgozás(t.Current())
t.Next()

12-1 Felsoroló által szolgáltatott elemek feldolgozása

Függetlenül azonban attól, hogy a felsoroló konkrét elemeket jár-e be


vagy csak generálja a felsorolt elemeket, a felsoroláshoz minden esetben
ugyanaz a négy művelet szükséges. A First() művelet indítja el a felsorolást
azzal, hogy rááll a felsorolás során először érintett elemre – feltéve, hogy van
ilyen. Minden további, tehát soron következő elemre a Next() művelet

467
segítségével tudunk ráállni. A Current() művelet a felsorolás alatt kijelölt
aktuális elemet (amire éppen „ráállt” a felsorolás) adja vissza. Az End() a
felsorolás során mindaddig hamis értéket ad, amíg van kijelölt aktuális elem,
a felsorolás végét viszont igaz visszaadott értékkel jelzi.

First(): 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

12-3. ábra. Egy felsoroló műveletei

Egy felsoroló megvalósítása elsősorban a műveleteinek


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

468
norm lesz, az e az eredeti sorozat első elemét, az f az eggyel rövidebb
sorozatot veszi fel értékként.
A szekvenciális inputfájl read műveletével kiváltható a felsorolás
klasszikus négy művelete. A fájl elemeinek felsorolása ugyanis előre-olvasási
technikával történik (12-3. ábra), amelyből látszik, hogy a First() és a Next()
műveleteket a read művelet helyettesíti, a Current() műveletet a read által
beolvasott elem (e), az End() művelet pedig az „end of file”eseményt a read
által beolvasott státuszt (st) segítségével vizsgálja.

st,e,f : read
st = norm
feldolgozás(e)
st,e,f : read

12-3. ábra. Előre-olvasási technika

Amikor a felsorolni kívánt elemek egy elképzelt sorozatot alkotnak,


azaz nem léteznek a valóságban, akkor többnyire külön osztállyal célszerű
definiálni a felsoroló típusát, amelyben külön metódusként szerepelnek a
felsorolás alapműveletei. Gyakori, hogy a First() művelet abban különbözik
csak a Next()-től, hogy néhány inicializáló lépést is tartalmaz: ilyenkor a First()
meghívja a Next() metódust. Mivel a Current() és az End() műveleteknek nem
szabad megváltoztatni a felsorolás állapotát, ezért ajánlott őket konstans
metódusként definiálni. Így e műveleteket akárhányszor meghívhatjuk
anélkül, hogy a felsorolást befolyásolnánk. Érdemes továbbá a Current() és az
End() metódusokat konstans műveletigényűre készíteni úgy, hogy a
felsorolás aktuális elemét illetve a felsorolás végét jelző logikai értéket
adattagként vesszük fel a felsoroló osztályába, és a Current() valamint az
End() csak ezen adattagok értékét adják vissza. A fentieken kívül az
osztálynak adattagja lesz a felsorolást generáló adat is. Ilyen adat például egy
természetes szám, ha annak prím osztóit kell felsorolni, vagy egy bitsorozat,
ha annak nyolcbitnyi szakaszaiban tárolt egész számok felsorolása a feladat.

469
A felsoroló műveletek implementálásánál figyelembe vehetjük (mert
ez könnyítést jelent), hogy a műveletek hatását nem kell minden esetre
definiálni. Például nem-definiált az, hogy a First() végrehajtása előtt (tehát a
felsorolás megkezdése előtt) illetve az End() igazra váltása után (azaz a
felsorolás befejezése után) mi a hatása a Next() és a Current() műveleteknek.
Általában nem definiált az sem, hogy mi történjen akkor, ha a First()
műveletet a felsorolás közben ismételten végrehajtjuk, vagy az End()
műveletet még az előtt használjuk, hogy a felsorolás a First() művelettel
elindult volna. A felsoroló felhasználási módja, a felsorolókra épülő
programozási tételek garantálják, hogy ezen „hiányosságok” ne okozzanak
futási hibát. Hasonlóképpen azt sem e műveletek implementálásának kell
garantálnia, hogy egy felsorolás véges lépésben biztosan befejeződjön.
Egy felsoroló osztály komponens tesztje speciális, egyszerűsített
eljárással történik, mivel a felsoroló műveleteinek egymáshoz való sorrendjét
a hívó program garantálja, ennél fogva nincs szükség a metódusok
variációinak tesztelésére, csak külön-külön az egyes metódusokéra.

Nyelvi háttér

A felsoroló típusának megvalósítását – történjen az saját osztállyal vagy


meglévő típusokkal – nem igényel az eddig használtakhoz képest újabb nyelvi
elemeket.
Egy szekvenciális inputfájl elemeinek felsorolásához és feldolgozásához
egyetlen read műveletet szoktunk használni. Ennek programozási nyelvi
változatai azonban többnyire nem adják vissza közvetlenül azt az
információt, hogy elértük-e már a felsorolás során a fájlvégét. A C-szerű
nyelvekben a sikertelen olvasás (amikor a fájl elemeit már mind felsoroltuk,
azaz a fájl kiürült) nem számít hibás műveletnek, végrajtását akár többször is
megismételhetjük. Az olvasás után le tudjuk kérdezni azt, hogy az sikerült-e
vagy sem (eof vagy fail). (Ettől lényegesen eltér a Pascal nyelv: ott előbb
kérdezzük, hogy vajon nincs-e még fájlvége, és csak pozitív válasz esetén
szabad olvasni.)

f >> e;

while( !f.fail()){

feldolgozás(e);

f >> e;

470
12-4. Előre-olvasási technika egy C++ nyelvi változata

A read művelet konkrét megjelenési formája a C++ nyelvben is függ a


szekvenciális inputfájl fizikai formájától és a beolvasandó adat típusától.
Mivel gyakori, hogy szöveges állományra épített szekvenciális inputfájl
elemeit soroljuk és dolgozzuk fel, ezért érdemes átismételni (lásd 5. fejezet),
hogy ezt miképpen lehet megtenni attól függően, hogy szöveges állomány
milyen formában tartalmazza az adatokat.
Szöveges állomány tartalmának karakterenkénti olvasását figyelhetjük
meg az alábbi kódrészletekben. Ebben a beolvasott karaktereket rögtön ki is
írjuk egy másik szöveges állományba, azaz itt egy karakterenkénti másolást
látunk.
ifstream x("inp.txt");

ofstream y("out.txt");

char ch;

for(x.get(ch); !x.fail(); x.get(ch)){

y.put(ch); // lehetne y << ch is

vagy
ifstream x("inp.txt");

ofstream y("out.txt");

char ch;

x.unsetf(ios::skipws);

for(x >> ch; !x.fail(); x >> ch){

y << ch; // lehetne y.put(ch) is

471
A gyakorlatban a fenti kódok helyett az alábbiakat szokták alkalmazni.
ifstream x("inp.txt");

ofstream y("out.txt");

char ch;

while(x.get(ch)){ y.put(ch); }

vagy
ifstream x("inp.txt");

ofstream y("out.txt");

char ch;

x.unsetf(ios::skipws);

while(x >> ch){ y << ch; }

A szöveges állománynak egynél több karakterből álló részét is be


tudjuk olvasni egy adatként, és egyetlen olvasó utasítás segítségével. Az
alábbi kóddal szöveges állományból elválasztó jelekkel szeparált egész
számokat olvasunk be, és megszámoljuk a páros számokat.
ifstream x("inp.txt");

int n, db = 0;

for(x >> n; !x.fail(); x >> n){

if(n%2 == 0) ++db;

vagy
ifstream x("inp.txt");

int n, db = 0;

while(x >> n){

472
if(n%2 == 0) ++db;

Technikailag semmi újdonságot nem tartalmaz az előzőekhez képest a


következő kódrészlet. Ez egy szöveg szavainak átlagos szóhosszát számolja ki.
Ebben elválasztó jelekkel (szóköz, tabulátor jel, sorvége jel) határolt
sztringeket (szavakat) olvasunk.
ifstream x("inp.txt");

string str;

int db, hossz;

db = hossz = 0;

for(x >> str; !x.fail(); x >> str){

hossz += str.size();

++db;

int atl = hossz/db;

vagy
ifstream x("inp.txt");

string str;

int db, hossz;

db = hossz = 0;

while(x >> str){

hossz += str.size();

++db;

473
int atl = hossz/db;

A szöveges állományok olvasásának harmadik lehetősége a soronkénti


olvasás. Ha egy sorban szereplő adatokat elválasztó jelekkel határolják, de
egy adaton belül nincs elválasztó jel, akkor a fenti technika ilyenkor is
alkalmazható. Az alábbi kódrészletben sorszám-név párokat olvasunk a
szöveges állományból. (Ez a program akkor is működik, ha a párok nem
soronként helyezkednek el a szöveges állományban.)
ifstream x("inp.txt");

int szam;

string nev;

for(x >> szam >> nev; !x.fail();

x >> szam >> nev){ ... }

vagy
ifstream x("inp.txt");

int szam;

string nev;

while(x >> szam >> nev){ ... }

Ha az elválasztó jelek nem szeparálják egyértelműen a beolvasott


értékeket (például egy név többtagú, azaz tartalmazhat szóközöket is), de
feltehetjük, hogy a szöveges állomány soraiban az egyes adatokat rögzített
pozíciókon helyeztük el, akkor a getline() utasítás segítségével egyszerre
egy egész sort olvashatunk be egy sztringbe, amelyből ki tudjuk hasítani a
megfelelő rész-sztringeket, hogy azokból aztán a kívánt értéket kinyerjük.
Például, ha egy szöveg minden sorában az első négy pozíción egy négyjegyű
szám, az azt követő húsz pozíción egy személy (szóközöket is tartalmazó)
neve áll, akkor az alábbi kódot használhatjuk a sorszám-név párok
felsorolására.

474
ifstream x("inp.txt");

string sor;

for(getline(x,sor); !x.fail();getline(x,sor)){

int szam = atoi(sor.substr( 0, 4).c_str());

string nev = sor.substr( 4,20);

...

vagy

ifstream x("inp.txt");

string sor;

while(getline(x,sor)){

int szam = atoi(sor.substr( 0, 4).c_str());

string nev = sor.substr( 4,20);

...

475
30. Feladat: Könyvtár

Egy szöveges állomány egy könyvtár adatait tartalmazza. Minden könyvről


ismerjük az azonosítóját, a szerzőjét, címét, kiadóját, kiadásának évét, ISBN
számát és azt, hogy hány példány van belőle jelenleg a könyvtárban. Egy
könyv adatai a szöveges állomány egy sorát foglalja el szigorú pozicionálási
szabályok mellett. Válogassuk ki a nulla példányszámú könyvek szerzőjét és
címét.

Specifikáció

A feladat egy kiválogatás: egy szekvenciális inputfájlból kell az adott


tulajdonságú elemeket kigyűjteni, és elhelyezni őket egy szekvenciális
outputfájlban.

A = ( x : SeqInFile(Könyv), y : SeqOutFile(Könyv2) )
Könyv = rec(azon:ℕ, szerző:String, cím:String,
kiadó:String, év:String, darab:ℕ, isbn:String)
Könyv2= rec(szerző:String, cím:String)
Ef = ( x=x' )
Uf = ( y dx.szerző , dx.cím )
dx x '
dx.darab 0

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

476
y : write(<dx.szerző,dx.cím>) SKIP
sx,dx,x : read

Implementálás

Az implementálásnál azt kell szem előtt tartani, hogy a szekvenciális inputfájl


hátterében a feladatban megadott formájú szöveges állomány áll, amelyre
definiálnunk kell az olvasás (read) műveletét. A szekvenciális outputfájl write
műveletét egy szöveges állományba történő írással kell megvalósítani.
Az implementációt kétféleképpen is elkészítjük. Először egy
egyszerűbb változatot mutatunk, ahol a read és write műveleteket csak
annyira különítjük el a kód többi részétől, hogy önálló alprogramokba
ágyazzuk őket. Másodszor egy kicsit „nagyobb feneket kerekítünk” a
megoldásnak, nevezetesen elkészítjük a feladat állapotterében szereplő két
fájl típusát definiáló osztályokat, és ezen osztályoknak lesz read illetve write
metódusa. Az első változat inkább egy procedurális szemléletű, a második
egy objektum orientált szemléletű megoldást tükröz.

Első megoldás szerkezete

Az első megoldás szerkezete nagyon egyszerű. A main.cpp néhány egyszerű


felhasználói típus (Könyv, Státusz) definiálása mellett három alprogramot
tartalmaz. A main függvény gondoskodik a szöveges állományok szekvenciális
input- és outputfájlként való megnyitásáról, tartalmazza az absztrakt
program kódját, amely hívja a másik kettő alprogramot.

Read()
main()
Write()

12-5. ábra. Alprogramok hívási láncai

477
Első megoldás kódja

A main függvényt megelőzi néhány fontos típusdefiníció. Nem definiáljuk


külön a Könyv2 típust, mert annak mezői a Könyv típus mezőinek részét
alkotják.

struct Book{

int id;

string author;

string title;

string publisher;

string year;

int piece;

string isbn;

};

enum Status{abnorm, norm};

A main függvény megpróbálja megnyitni a bemeneti szöveges


állományt és létrehozni a kimeneti szöveges állományt. Ha mindkét
tevékenység sikerül, akkor kerül végrehajtásra az absztrakt programban
rögzített kiválogatás.

int main()

ifstream x("inp.txt");

if (x.fail() ) {

cerr << "Nincs input file!\n";

478
char ch; cin>>ch; exit(1);

ofstream y("out.txt");

if (y.fail() ) {

cerr << "Nincs output fájl!\n";

char ch; cin>>ch; exit(1);

Book dx;

Status sx;

for(Read(x,dx,sx); norm==sx; Read(x,dx,sx)) {

if (0 == dx.count) {

Write(y, dx.author, dx.title);

return 0;

A Read() művelet a klasszikus szekvenciális inputfájlból való olvasás,


amely kihasználja, hogy a bemeneti szöveges állomány formája kötött, ezért
egy teljes sor sikeres beolvasása után a sorból, mint sztringből ki lehet vágni
az egyes részadatokat.

void Read(ifstream &x, Book &dx, Status &sx)

string sor;

479
getline(x,sor,'\n');

if (!x.fail()) {

sx = norm;

dx.id = atoi(sor.substr( 0, 4).c_str());

dx.author = sor.substr( 5,14);

dx.title = sor.substr(21,19);

dx.publisher = sor.substr(42,14);

dx.year = sor.substr(58, 4);

dx.count = atoi(sor.substr(63, 3).c_str());

dx.isbn = sor.substr(67,14);

else sx = abnorm;

A Write() művelet egy új sort illeszt a kimeneti szöveges


állományhoz.

void Write(ofstream &y, const string &author, const


string &title)

y << setw(14) << author << ' '

<< setw(19) << title << endl;

Második megoldás komponens szerkezete

480
Ebben a megvalósításban két külön osztály írja le a szekvenciális inputfájl
(Stock) és a szekvenciális outputfájl (Result) típusát. Ezeket külön
csomagokba helyezzük.

main.cpp stock.h-stock.cpp result.h-result.cpp

class Stock class Result


• main() • Stock() • Result()
• Read() • Write()
• ~Stock() • ~Result()

12-6. ábra. Komponens szerkezet

Második megoldás függvények hívási láncolata

A vezérlést, azaz a komponensek egyes függvényeinek megfelelő sorrendben


történő meghívását a main függvény biztosítja.

Stock()

Read()

~Stock()
main()
Result()

Write()

~Result()

12-7. ábra. Alprogramok hívási láncai

481
Második megoldás osztályai

A Törzs típust leíró Stock osztály egy objektumát, egy törzsfájlt egy
ifstream típusú objektummal reprezentáljuk. Ezt az osztály konstruktora
nyitja meg és az inline definíciójú destruktora zárja be. Az osztály ezeken
kívül még a Read() metódust tartalmazza: ez a soron következő könyv
adatainak beolvasását végzi.
Az osztály a Book és Status típusok definíciójával együtt a stock.h
állományba tesszük.

class Stock{

public:

Stock(std::string fname);

void Read(Book &df, Status &sf);

private:

std::ifstream f;

};

A stock.cpp állományban helyeztük el a konstruktor és a Read()


metódus implementációját. A konstruktor – amennyiben nem kapja meg
bemenetként – megkérdezi a megnyitandó szöveges állomány nevét, majd
megkísérli azt megnyitni.

Stock::Stock(string fname = "")

if ( fname.size()<1 ) {

cout << "Add meg a törzsfájl nevét:" ;

cin >> fname;

482
}

f.open(fname.c_str());

if ( f.fail() ){

cerr << "Nincs törzs fájl" <<endl;

char ch; cin>>ch; exit(2);

A Read() metódus törzse szóról szóra megegyezik az első változat


Read() műveletével. Csak a metódus paraméterezése tér el attól, és ennek
megfelelően a hívásának a formája.

void Stock::Read(Book &df, Status &sf)

string sor;

getline(f, sor,'\n');

if (!f.fail()) {

sf = norm;

df.id = atoi(sor.substr( 0, 4).c_str());

483
df.author = sor.substr( 5,14);

df.title = sor.substr(21,19);

df.publisher = sor.substr(42,14);

df.year = sor.substr(58, 4);

df.piece = atoi(sor.substr(63, 3).c_str());

df.isbn = sor.substr(67,14);

else sf = abnorm;

Az Eredm típust leíró Result osztály a result.h állományba kerül.

class Result{

public:

Result(std::string fname);

void Write(const std::string &author,

const std::string &title);

private:

std::ofstream f;

};

Az result.cpp állományban helyeztük el a konstruktor és a Write()


metódus implementációját. A konstruktor szinte szó szerinti mása a Stock
osztály konstruktorának.

484
Result::Result(string fname = "")

if ( fname.size()<1 ) {

cout << "Add meg a törzsfájl nevét:" ;

cin >> fname;

485
f.open(fname.c_str());

if ( f.fail() ){

cerr << "Nincs törzs fájl" <<endl;

char ch; cin>>ch; exit(2);

A Write() metódus lényegében megegyezik az első változat Write()


műveletével.

void Result::Write(const string &author,

const string &title)

f << setw(14) << author << ' '

<< setw(19) << title << endl;

Második megoldás főprogramja

A fent bevezetett osztályoknak köszönhetően a main függvény kizárólag az


absztrakt program kódját tartalmazza, ezáltal végletesen mellőz minden, a
konkrét implementációval kapcsolatos részletet. Ezek ugyanis az
osztályokban vannak elrejtve.

int main()

486
{

Stock x("inp.txt");

Result y("out.txt");

Book dx;

Status sx;

for(x.Read(dx,sx); norm==sx; x.Read(dx,sx)) {

if (0==dx.piece) y.Write(dx.author, dx.title);

return 0;

Tesztelés

A két változat fekete doboz tesztesetei megegyeznek: Ennek alapját a


kiválogatás (összegzés) adja, amelyiknél elsősorban az „intervallum” tesztre
(amit most a szekvenciális inputfájl vált ki) kell figyelni.
1. Üres törzsfájl esete.
2. Nem üres, csupa nulla darabszámú könyv a törzsfájlban.
3. Ne üres, csupa nem-nulla darabszámú könyv a törzsfájlban.
4. Általános eset nem üres törzsfájlra.
5. Az első és az utolsó könyv darabszáma nulla a törzsfájlban.
A fehér doboz tesztelése sem tér el egymástól a két verziónak. Az, amit
ennek keretében külön meg kell vizsgálni, az a fájlnyitásoknak, illetve az
írás/olvasásnak a megfelelő működése és hibakezelése. Ebben a tekintetben
a második változat annyival tud többet, hogy ha a fájl nevét nem adjuk meg,

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

488
Teljes program

Az első változat teljes kódja:


#include <fstream>

#include <iostream>

#include <iomanip>

#include <string>

using namespace std;

struct Book{

int id;

string author;

string title;

string publisher;

string year;

int piece;

string isbn;

};

enum Status{abnorm, norm};

void Read(ifstream &x, Book &dx, Status &sx);

void Write(ofstream &x, const string &author,

const string &title);

489
int main()

ifstream x("inp.txt");

if (x.fail() ) {

cerr << "Nincs input file!\n";

char ch; cin>>ch; return 1;

ofstream y("out.txt");

if (y.fail() ) {

cerr << " Nincs output fájl!\n";

char ch; cin>>ch; return 1;

Book dx;

Status sx;

for(Read(x,dx,sx); norm==sx; Read(x,dx,sx)) {

if (0 == dx.piece) {

Write(y, dx.author, dx.title);

return 0;

void Read(ifstream &x, Book &dx, Status &sx)

490
string sor;

getline(x,sor,'\n');

if (!x.fail()) {

sx = norm;

dx.id = atoi(sor.substr( 0, 4).c_str());

dx.author = sor.substr( 5,14);

dx.title = sor.substr(21,19);

dx.publisher = sor.substr(42,14);

dx.year = sor.substr(58, 4);

dx.piece = atoi(sor.substr(63, 3).c_str());

dx.isbn = sor.substr(67,14);

else sx = abnorm;

void Write(ofstream &y, const string &author,

const string &title)

y << setw(14) << author<< ' '

<< setw(19) << title << endl;

491
A második változat teljes kódja:

main.cpp:
#include <fstream>

#include <string>

#include "stock.h"

#include "result.h"

using namespace std;

int main()

Stock x("inp.txt");

Result y("out.txt");

Book dx;

Status sx;

for(x.Read(dx,sx); norm==sx; x.Read(dx,sx)) {

if (0 == dx.piece) y.Write(dx.author, dx.title);

return 0;

stock.h:
#ifndef _STOCK_

#define _STOCK_

#include <fstream>

492
#include <string>

struct Book {

int id;

std::string author;

std::string title;

std::string publisher;

std::string year;

int piece;

std::string isbn;

};

enum Status {abnorm,norm};

class Stock{

public:

Stock(std::string fname);

void Read(Book &df, Status &sf);

private:

std::ifstream f;

};

#endif

stock.cpp:
#include "stock.h"

493
#include <iostream>

#include <cstdlib>

using namespace std;

Stock::Stock(string fname = "")

if ( fname.size()<1 ) {

cout << "Add meg a törzsfájl nevét:" ;

cin >> fname;

f.open(fname.c_str());

if ( f.fail() ){

cerr << "Nincs törzs fájl" <<endl;

char ch; cin>>ch;

exit(2);

void Stock::Read(Book &df, Status &sf)

string sor;

getline(f, sor,'\n');

if (!f.fail()) {

494
sf = norm;

df.id = atoi(sor.substr( 0, 4).c_str());

df.author = sor.substr( 5,14);

df.title = sor.substr(21,19);

df.publisher = sor.substr(42,14);

df.year = sor.substr(58, 4);

df.piece = atoi(sor.substr(63, 3).c_str());

df.isbn = sor.substr(67,14);

else sf = abnorm;

495
result.h:
#ifndef _RESULT_

#define _RESULT_

#include <fstream>

#include <string>

class Result{

public:

Result(std::string fname);

void Write(const std::string &author,

const std::string &title);

private:

std::ofstream f;

};

#endif

result.cpp:
#include "result.h"

#include <iostream>

#include <cstdlib>

#include <iomanip>

using namespace std;

496
Result::Result(string fname = "")

if ( fname.size()<1 ) {

cout << "Add meg a törzsfájl nevét:" ;

cin >> fname;

f.open(fname.c_str());

if ( f.fail() ){

cerr << "Nincs törzs fájl" <<endl;

char ch; cin>>ch;

exit(2);

void Result::Write(const string &author,

const string &title)

f << setw(14) << author << ' '

<< setw(19) << title << endl;

497
31. Feladat: Havi átlag-hőmérséklet

Adott egy szöveges állományban egy adott időszak napi


átlaghőmérsékleteinek sorozata. Az állomány minden sora egy év-hó-nap
(eehhnn) formátumban megadott dátumot tartalmaz, amelyet egy szóköz,
majd egy hőmérsékleti érték követ. Az állomány dátum szerint növekedően
rendezett. Hány olyan egymást követő hónap-pár van, ahol a havi
átlaghőmérséklet megegyezik?

Specifikáció

A feladat egy számlálás, amelyet azonban nem az állományban megadott


dátum-hőmérséklet párok sorozata felett kell közvetlenül értelmezni, hanem
azon átlaghőmérséklet-párok sorozata felett, amelyek két szomszédos hónap
havi átlaghőmérsékleteiből állnak. Bevezetjük tehát e szám-párokat
szolgáltató absztrakt felsorolót (enor(Pár)).

A = ( t : enor(Pár), darab : ℕ )
Pár = rec(akt:ℝ, elő:ℝ)
Ef = ( t=t' )
Uf = ( darab 1 )
aktpár t '
aktpár.akt aktpár.elő
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()

498
t.Next() ~ x.Next(); tvége:=x.End()
ha x.End() akkor aktpár.előző:=aktpár.akt
aktpár.akt:=x.Current()
t.Current() ~ aktpár
t.End() ~ tvége

Az x felsoroló megvalósításához a szöveges állomány dá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ó
nap.hó hó
össz , st , nap, f: nap.hő
nap ( nap, f )
nap.hó hó
db, st , nap, f: 1
nap ( nap, f )

499
havi:=össz/db
x.Current() ~ havi
x.End() ~ xvége

Absztrakt program

A feladatot megoldása több, egymásra épülő rétegből tevődik össze.


Legfelül egy számlálás, amelyik a t felsoroló működésére épül:

darab := 0
t.First()

t.End()
t.Current().akt=t.Current().elő
darab:=darab+1 SKIP
t.Next()

A t felsoroló típusa alkotja a következő szintet, amelyhez viszont az x


felsoroló típusa szükséges, amely a harmadik szint, ahol az adatfolyam
objektumra lesz szükség, amely segítségével a szöveges állomány olvasható.
Itt valójában átugrunk egy szintet, mert nem valósítjuk meg önálló
osztályként annak a szekvenciális inputfájlnak a típusát, amellyel a szöveges
állomány sorait tudjuk kiolvasni. Ezért a szekvenciális inputfájlnak a read
művelete az x felsoroló típusának szintjén kerül majd megadásra.
A t felsoroló műveleteinek implementálása nem igényel ciklust, ezért a
művelet-implementációk algoritmusát nem részletezzük. Az x felsoroló
műveletei közül csak a x.Next() műveletnek érdemes külön felírni az
absztrakt programját. Ez egy elágazás, amelyen belül két feltétel fennállásáig
(amíg ugyanazon hónap hőmérsékleteit vizsgáljuk) tartó összegzés szerepel:
egyik összeadja a napi átlaghőmérsékleteket, a másik megszámolja, hány nap
van a hónapban. Mindkét összegzés ugyanazon a két ponton tér el egy
szekvenciális inputfájl elemeinek nevezetes felsorolására épülő összegzéstől.

500
Egyrészt az End() műveletük már a fájlvége előtt is igaz lehet, ha hónap
végére értünk, tehát a ciklusfeltétel kiegészül az aktuális hónap figyelésével.
Másrészt nem igényelnek előre olvasást, mert a hónap legelső napját vagy az
x.First(), vagy az előző x.Next() végén már beolvastuk. A két összegzést –
lévén azonos szerkezetűek – egyetlen ciklusba vonjuk össze.

xvége := st=abnorm
st=norm
hó, össz, db:=nap.hó, 0, 0

st=norm hó=nap.hó SKIP

össz, db := össz+nap.hő, db+1


st, nap, f : read
havi:=össz/db

Implementálás

Az implementációban önálló osztályként fogalmazzuk meg az enor(Pár) és az


enor(ℝ) típusát, a szöveges állománybeli napi adatok felsorolásához pedig
egy Read() alprogramot fogunk használni. Az osztályok publikus elemei a
bejáró műveletek lesznek.
Figyelnünk kell arra, hogy a tervezésnél előállt modulok megfelelő
módon hivatkozzanak egymásra, és egy-egy nyelvi elem láthatósága
biztosítva legyen ott, ahol használni akarjuk.
Ezen túlmenően néhány egyszerű átalakítás is végzünk. Ilyen például
az, hogy egy double-ként definiált valós változónak kezdőértéke nem 0
hanem 0.0 lesz, vagy, hogy segédváltozók bevezetésével csökkentjük egy-egy
metódus ismételt meghívásainak számát.

A program komponens szerkezete

A main.cpp main függvénye tartalmazza az absztrakt megoldást


alkotó számlálást. Ez a program a Pair_Enor (enor(Pár)) osztályt használja,

501
ezért a main.cpp állományba be kell inklúdolni a pair_enor.h állományt.
A Pair_Enor osztálynak a Month_Average_Enor (enor(ℝ)) 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.

pair_enor.h- average_enor.h-
main.cpp
pair_enor.cpp average_enor.cpp

class class
• main()
Pair_Enor Average_Enor
• First() • First()
• Next() • Next()
• End() • End()
• Current() • Current()
• Open()
• Read()

12-8. ábra. Komponens szerkezet

Főprogram kódolása

A tervezés során bevezetett absztrakciós szinteknek köszönhetően a main


függvény a lehető legegyszerűbb lett, a legfelső szint számlálását
tartalmazza:

int main()

502
Pair_Enor t("input.txt");

int count = 0;

for(t.First(); !t.End(); t.Next()){

if(t.Current().curr == t.Current().prev)

++count;

cout << "Azonos hőmérséklet párok száma: "

<< count;

return 0;

A main függvény hívja a Pair_Enor osztály metódusait. A konstruktornak a


megnyitandó szöveges állomány nevét adja át.

Pair_Enor()

First()

main() Next()

End()

Current()

12-9. ábra. Alprogramok hívási láncai

Pair_Enor osztály

503
A tervezésben enor(Pár)-ként definiált Pair_Enor osztály a t felsoroló
típusát határozza meg. Az osztály kódja külön fej- és forrásállományba kerül.
Az osztály definíciója előtt definiálni kell a Pair (Pár) típust.

struct Pair{

double prev;

double curr;

};

class Pair_Enor{

private:

Month_Average_Enor x;

bool end;

Pair current;

public:

Pair_Enor(const std::string &str)

{ x.Open(str); }

void First();

void Next();

Pair Current() const { return current; }

bool End() const { return end; }

};

A Pair_Enor osztály definíciója inline módon tartalmazza a


Current(), az End() és a konstruktor implementálását.

504
A tervezésnél az osztály konstruktoráról nem esett szó. Ez egy sztringet
(szöveges állomány neve) kap bemenetként, és ezzel hívja meg a
Month_Average_Enor osztály Open() függvényét, amely a szöveges
állományt nyitja majd meg.

Pair_Enor() Open()

12-10. ábra. Alprogramok hívási láncai

A First() és a Next() műveletek kódjában egy apró változtatás a


tervhez képest például az, hogy a második és harmadik if utasítás feltétele az
end segédváltozóra hivatkozik, nem hívja meg újra és újra az x.End()
metódust.
A Pair_Enor osztály First() és a Next() metódusai a
Month_Average_Enor osztály bejáró metódusait hívják.

First()

Next()
First()
End()

Current()

Next()

Next() End()

Current()

12-11. ábra. Alprogramok hívási láncai

505
void Pair_Enor::First() {

x.First();

if(!x.End())current.prev = x.Current();

x.Next();

end = x.End();

if(!end)current.curr = x.Current();

void Pair_Enor::Next() {

x.Next();

end = x.End();

if (!end) {

current.prev = current.curr;

current.curr = x.Current();

506
Month_Average_Enor osztály

A tervezésben enor(ℝ)-ként definiált osztály az x felsoroló típusát határozza


meg. Az osztály kódja külön fej- és forrásállományba kerül. Itt
implementáljuk a szöveges állományból történő Read() olvasó műveletet is,
amely egy Day (Nap) típusú elemet olvas az állomány egy sorából és beállítja
az olvasás státuszát is. Meg kell tehát adnunk a Status és a Day típust.

enum Status { abnorm, norm };

struct Day{

int month;

double term;

};

Jól láthatóak az osztály tervezésekor már említett privát adattagok: az


adatfolyam (f), az olvasás két segédadata (day, st), az olvasás művelete
(Read()), a havi átlaghőmérsékletek felsorolásának végét jelző logikai
változó (end), valamint az utoljára felsorolt hónap átlag hőmérséklete
(avrterm). A láthatósági szabályok miatt nem kell nevében
megkülönböztetni az itteni end tagot a Pair_Enor end tagjától. (A
tervezésnél ezekre még külön elnevezést használtunk.)

class Month_Average_Enor{

private:

std::ifstream f;

Day day;

Status st;

507
bool end;

double avrterm;

void Read();

public:

void Open(const std::string &str);

void First() { Read(); Next(); }

void Next();

double Current() const { return avrterm; }

bool End() const { return end;}

};

Az osztály definíciója inline módon tartalmazza a destruktor, a


First(), a Current() és az End() implementálását. Ezek, csakúgy, mint a
Next() művelet kódja, megfelelnek a tervezésnél megadott
implementációnak.

void Month_Average_Enor::Next()

end = abnorm == st;

if(!end){

avrterm = 0.0;

int c = 0;

for(int month = day.month; norm == st &&

month == day.month; Read() ){

508
avrterm += day.term;

++c;

avrterm /= c;

Az Open() bemenetként a megnyitandó szöveges állomány nevét


kapja meg, és ha ez létezik, akkor egy ifstream adatfolyamot nyit erre az
állományra, amelyet a Month_Average_Enor destruktora zárja be.

void Month_Average_Enor::Open(const string &str)

f.open(str.c_str());

if(f.fail()){

cout << "Inputfajl hiba!\n";

exit(1);

A Read() művelet az állomány soron következő sorát beolvasva


kinyeri abból az adott nap hónapjának számát és a napi átlaghőmérsékletet.
Ennek a műveletnek a paraméterei a Month_Average_Enor privát
adattagjai, nevezetesen a f adatcsatorna, a day adat és az st státusz.

void Month_Average_Enor::Read()

509
{

string date;

f >> date;

if(!f.fail()){

st = norm;

day.month = atoi(date.substr(2,2).c_str());

f >> day.term;

else st = abnorm;

Tesztelés

Fekete doboz tesztesetek: (számlálás és összegzések)


1. Üres állomány.
2. Egyetlen nap adatait (egy sort) tartalmazó állomány.
3. Egyetlen hónap adatait tartalmazó állomány.
4. Több, nem azonos átlaghőmérsékletű hónap adatait tartalmazó
állomány.
5. Olyan állomány, ahol csak az első két hónap átlaghőmérséklete
azonos.
6. Olyan állomány, ahol csak az utolsó két hónap átlaghőmérséklete
azonos.
7. Általános eset, ahol több egymás utáni hónap-pár
átlaghőmérséklete is azonos.
8. Általános eset, ahol minden hónap átlaghőmérséklete azonos.

Fehér doboz tesztesetek a fentieken kívül:

510
1. Nem létező állomány név.

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

511
Teljes program

main.cpp:
#include <iostream>

#include "pair_enor.h"

using namespace std;

int main()

Pair_Enor t("input.txt");

int count = 0;

for(t.First(); !t.End(); t.Next()){

if(t.Current().curr == t.Current().prev)

++count;

cout << "Azonos hőmérséklet-párok száma: "

<< count;

return 0;

512
513
pair_enor.h:
#ifndef PAIR_ENOR_H

#define PAIR_ENOR_H

#include <string>

#include "month_average_enor.h"

struct Pair{

double prev;

double curr;

};

class Pair_Enor{

private:

Month_Average_Enor x;

bool end;

Pair current;

public:

Pair_Enor(const std::string &str)

{ x.Open(str); }

void First();

void Next();

Pair Current() const { return current; }

bool End() const { return end; }

};

#endif

514
pair_enor.cpp:
#include "pair_enor.h"

void Pair_Enor::First() {

x.First();

if(!x.End())current.prev = x.Current();

x.Next();

end = x.End();

if(!end)current.curr = x.Current();

void Pair_Enor::Next() {

x.Next();

end = x.End();

if (!end) {

current.prev = current.curr;

current.curr = x.Current();

515
month_average_enor.h:
#ifndef MONTH_AVERAGE_ENOR_H

#define MONTH_AVERAGE_ENOR_H

#include <fstream>

#include <string>

enum Status { abnorm, norm };

struct Day{

int month;

double term;

};

class Month_Average_Enor{

private:

std::ifstream f;

Day day;

Status st;

bool end;

double avrterm;

void Read();

public:

void Open(const std::string &str);

void First() { Read(); Next(); }

void Next();

516
double Current() const { return avrterm; }

bool End() const { return end;}

};

#endif

month_average_enor.cpp:
#include "month_average_enor.h"

#include <iostream>

#include <cstdlib>

using namespace std;

void Month_Average_Enor::Open(const string &str)

f.open(str.c_str());

if(f.fail()){

cout << "Inputfájl hiba!\n";

exit(1);

517
void Month_Average_Enor::Next()

end = abnorm == st;

if(!end){

avrterm = 0.0;

int db = 0;

for(int month = day.month; norm == st &&

month == day.month; Read() ){

avrterm += day.term;

++db;

avrterm /= db;

void Month_Average_Enor::Read()

string date;

f >> date;

if(!f.fail()){

st = norm;

day.month = atoi(date.substr(2,2).c_str());

f >> day.term;

518
else st = abnorm;

519
32. Feladat: Bekezdések

Egy szöveges állományban bekezdésekre tördelt szöveg található. Egy


bekezdés egy vagy több nem üres sorból áll. A bekezdéseket üres sorok vagy
az állomány eleje illetve vége határolja.
Melyik a leggazdagabb bekezdés, azaz hányadik az a legalább három soros
bekezdés, amelyik tartalmazza az „alma” szót önmagában vagy valamilyen
szóösszetételben és az ilyen bekezdések közül nála a legnagyobb a szavak
számának és a sorok számának hányadosa? A szövegben egyik szó sincs több
sorra tördelve, a szavakat szóközök, tabulátor-jelek és sorvége-jelek (akár
egymás után több is) választhatja el egymástól.

Specifikáció

A feladat megoldása egy feltételes maximumkeresés, amelyik azt a bekezdést


keresi meg az „alma” szót tartalmazó bekezdések között, ahol a legnagyobb a
szavak számának és a sorok számának hányadosa. Ehhez egy olyan absztrakt
felsorolóra van szükségünk, amely az egyes bekezdések statisztikáit képes
megadni: a bekezdés sorszámát, sorainak számát, szavainak számát,
valamint, hogy szerepel-e benne az „alma” szó.

A = ( t : enor(Bekezdés), l : , max : ℝ, ind : ℕ )


Bekezdés = rec(sorsz:ℕ, szó:ℕ, sor:ℕ, alma: )
Ef = ( t=t' )
Uf = ( l , max, elem max e.szó / e.sor
e t'
e.alma e.sor 3
l ind = elem.sorsz )

A t felsoroló First() illetve Next() művelete egy bekezdésnyit olvas a


szövegből. Ehhez számon tartják, hogy hányadik bekezdésnél tartunk, először
átlépik az üres sorokat, megszámolják a szavak és a sorok számát és figyelik
az „alma” szó előfordulását. Mindehhez a szöveges állomány sorait kell
tudnunk bejárni. Ehhez a szöveges állományt szekvenciális inputfájlként
(f:seqinfile(Sztring)) kell kezelni, így az állományból könnyű egy teljes sort

520
beolvasni. El kell készítenünk egy read műveletet, amely beállítja az olvasás
státuszát és a beolvasott sort egy sztring típusú változóba helyezi (st:Státusz,
sor:Sztring). Ezen kívül tárolnunk kell a legutoljára beolvasott bekezdés
statisztikáját (akt:Bekezdés), és azt a logikai értéket (vége: ), amely jelzi, ha
már nincs több bekezdés. A t felsoroló műveleteit az alábbiak szerint
implementáljuk:
t.First() ~ akt.sorsz:=0
st, sor, f:read
t.Next()
t.Current() ~ akt
t.End() ~ vége
t.Next() ~ st , sor , f select üres ( sor )
sor ( sor , f )

vége:= st=abnorm
ha st=norm akkor
akt.sorsz:= akt.sorsz+1
üres ( sor )
akt.alma, st , sor , f: " alma" sor
sor ( sor , f )
üres( sor )
akt.sor , st , sor , f: 1
sor ( sor , f )
üres( sor )
akt.szó, st , sor , f: sorbeli szavak száma
sor ( sor , f )

521
Absztrakt program

A feladatot megoldó feltételes maximumkeresés:

l:= hamis; t.First()

t.End()

bek:=t.Current()

(bek.alma bek.alma bek.sor≥3 l bek.alma bek.sor≥3


bek.sor≥3 ) l

SKIP bek.szó/bek.sor>max l, max, ind :=

max, ind:= SKIP igaz, bek.szó/bek.sor,


bek.szó/bek.sor, bek.sorsz
bek.sorsz

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

522
(sor-beli szavak száma)
st, sor, f : read

Az akt.alma, akt.sor és akt.szó értékét meghatározó összegzések


ciklusait egy ciklusba vonjuk össze. Az üres(sor), „alma” sor és a sor-beli
szavak száma kifejezéseket majd az implementációban pontosítjuk.

Implementálás

A t felsoroló típusát egy osztály segítségével célszerű megadni. Az


osztályok privát tagjait a felsorolót reprezentáló elemek alkotják, valamint a
szöveges állomány egy sorát kiolvasó read művelet.
Ezen a túlmenően az implementálásra már csak annyi maradt, hogy
megtaláljuk a helyes kódot az olyan részletek leírására, mint az üres(sor),
„alma” sor és a sor-beli szavak száma.

A program komponens szerkezete

A feltételes maximumkeresést a main.cpp forrásállomány main


függvényben helyezzük el a kiírással együtt, a felsoroló típusát leíró osztályt
pedig külön fej- és forrásállományban (enor.h, enor.cpp) adjuk meg. A
main.cpp állománynak be kell inklúdolnia az enor.h állományt.
A program komponens szerkezete meglehetősen egyszerű.

523
main.cpp enor.h-enor.cpp

class Enor
• main() • Enor()
• First()
• Next()
• End()
• Current()
• ~Enor()

12-12. ábra. Komponens szerkezet

Főprogram kódolása

int main()

Enor t("input.txt");

int ind;

double max;

bool l = false;

for(t.First(); !t.End(); t.Next()){

Statistic bek = t.Current();

if(!(bek.apple && bek.line>=3)) continue;

double rate

= (double)bek.word/(double)bek.line;

if(l && rate>max) {

524
max = rate;

ind = bek.no;

} else {

l = true;

max = rate;

ind = bek.no;

if (l) cout << "A \"leggazdagabb\" bekezdés a "

<< ind << "-dik\n"

<< "arány: " << max << endl;

else cout << "Nincs \"gazdag\" bekezdés!\n";

return 0;

A main függvény hívja az Enor osztály metódusait. A konstruktornak a


megnyitandó szöveges állomány nevét adja át. A függvény befejeződésekor a
destruktor is meghívódik.

525
Enor()

First()

Next()
main()
End()

Current

Current()

12-13. ábra. Alprogramok hívási láncai

Enor osztály

A bekezdések bejárásának típusát leíró osztály előtt definiálnunk kell a


bekezdés statisztikáját megadó Statistic típust (a tervezésben ez
Bekezdés néven szerepelt). Mivel itt implementáljuk a szöveges állományból
történő olvasó műveletet is, ezért be kell vezetni a Status típust.

enum Status { abnorm, norm };

struct Statistic{

bool apple;

int word;

int line;

int no;

};

526
Az Enor osztály privát tagjai között találjuk a felsorolót reprezentáló
ifstream típusú f objektumot (szekvenciális inputfájl), az aktuális bekezdés
statisztikáját tartalmazó current változót (tervezésnél akt néven szerepelt),
és a felsorolás végét jelző end flag-et (vége). Ezek kiegészülnek a szöveges
állományból való olvasás adataival: az aktuális sort tartalmazó line nevű
istringstream taggal (sor) és az utolsó olvasás státuszával (st). Privát
elem lesz a Read() olvasó metódus is.

class Enor{

private:

std::ifstream f;

std::istringstream line;

Status st;

bool end;

Statistic current;

void Read();

Az osztály definíciója inline módon tartalmazza a destruktor, a


First(), a Current() és az End() implementálását. Ezek, csakúgy, mint a
Next() művelet kódja, megfelelnek a tervezésnél megadott
implementációnak.

public:

Enor(const std::string &str);

void First()

{ current.no = 0; Read(); Next();}

void Next();

527
Statistic Current() const { return current;}

bool End() const { return end;}

};

A Next() metódus megvalósításánál meg kell vizsgálni, hogy a C++


nyelv milyen lehetőségeket kínál az üres(sor), az „alma” 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;

528
for(;norm==st && line.str().size() != 0;

Read()){

++current.line;

string w;

for(line>>w; !line.fail(); line>>w){

++current.word;

current.apple = current.apple ||

w.find("alma")!=string::npos;

Az Enor konstruktora nyit egy ifstream csatornát a paraméterként


megadott nevű szöveges állományra.

void Enor::Enor(const string &str)

f.open(str.c_str());

if(f.fail()){

cout << "Inputfájl hiba!\n";

exit(1);

529
A Read() művelet az állomány soron következő sorát olvassa be, mint
egy sztringet, és ezt alakítja át istringstream típusú adattá. Erre szolgál a
line.clear() és a line.str() metódus.

void Enor::Read()

string str;

getline(f,str,'\n');

if(!f.fail()){

st = norm;

line.clear();

line.str(str);

else st = abnorm;

Tesztelés

Fekete doboz tesztesetek: (Feltételes maximum keresés, azon belül


kiválasztás és összegzések.)
1. Üres állomány
2. Egyetlen legalább három soros bekezdés, ahol nincs „alma” szó.
3. Egyetlen egy soros bekezdés, amely tartalmaz „alma” szót.
4. Egyetlen két soros bekezdés (előtte és utána több üres sorral),
amelynek minden sora tartalmaz „alma” szót.

530
5. Egyetlen legalább három soros bekezdés (előtte és utána több üres
sorral), amelynek az első sorának első szava „alma”.
6. Egyetlen legalább három soros bekezdés (előtte és utána több üres
sorral), amelynek az első sorának utolsó szava „alma”.
7. Egyetlen legalább három soros bekezdés (előtte és utána több üres
sorral), amely utolsó sorának első szava „alma”.
8. Egyetlen legalább három soros bekezdés (előtte és utána több üres
sorral), amely utolsó sorának utolsó szava „alma”.
9. Egyetlen legalább három soros bekezdés (előtte és utána több üres
sorral), amely középső sorának első szava „alma”.
10. Egyetlen legalább három soros bekezdés (előtte és utána több üres
sorral), amely középső sorának utolsó szava „alma”.
11. Több legalább három soros, „alma” szavas bekezdés, a bekezdések
előtt, között, utána több üres sorral, és a legelsőben a legnagyobb a
szó/sor arány.
12. Több legalább három soros, „alma” szavas bekezdés, a bekezdések
előtt, között, utána több üres sorral, és a legutolsóban a legnagyobb
a szó/sor arány.
13. Több legalább három soros, „alma” szavas bekezdés, a bekezdések
előtt, között, utána több üres sorral, és egy középsőben a
legnagyobb a szó/sor arány.
14. Több legalább három soros, „alma” szavas bekezdés, a bekezdések
előtt, között, utána több üres sorral, és minden bekezdés egyformán
gazdag.
15. Általános eset több bekezdéssel, a bekezdések előtt, között, utána
több üres sorral.
Fehér doboz tesztesetek a fentieken kívül:
1. Nem létező állomány név.

Komponens tesztet nem kell külön csinálni. Egyrészt itt egy felsorolót
megvalósító osztállyal van dolgunk, amelynél a metódusok variációs tesztje
elhagyható, másrészt a metódusok a Next() kivételével egyszerű beolvasást

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

532
Teljes program

main.cpp:
#include <iostream>

#include "enor.h"

using namespace std;

int main()

Enor t("input.txt");

int ind;

double max;

bool l = false;

for(t.First(); !t.End(); t.Next()){

Statistic bek = t.Current();

if(!(bek.apple && bek.line>=3)) continue;

double rate

= (double)bek.word/(double)bek.line;

if(l && rate>max) {

max = rate;

ind = bek.no;

else {

l = true;

533
max = rate;

ind = bek.no;

if (l) cout << "A \"leggazdagabb\" bekezdés a "

<< ind << "-dik\n"

<< "arány: " << max << endl;

else cout << "Nincs \"gazdag\" bekezdés!\n";

return 0;

534
enor.h:
#ifndef ENOR_H

#define ENOR_H

#include <fstream>

#include <string>

#include <sstream>

enum Status { abnorm, norm };

struct Statistic{

bool apple;

int word;

int line;

int no;

};

class Enor{

private:

std::ifstream f;

std::stringstream line;

Status st;

bool end;

Statistic current;

void Read();

535
public:

Enor(const std::string &str);

void First()

{ current.no = 0; Read(); Next();}

void Next();

Statistic Current() const { return current;}

bool End() const { return end;}

};

#endif

536
enor.cpp:
#include "enor.h"

#include <iostream>

#include <cstdlib>

using namespace std;

Enor::Enor(const string &str)

f.open(str.c_str());

if(f.fail()){

cout << "Inputfájl hiba!\n";

exit(1);

void Enor::Next()

for(;norm==st && line.str().size()==0; Read());

end = abnorm == st;

if(!end){

++current.no;

current.word = current.line = 0;

current.apple = false;

for(;norm==st && line.str().size() != 0;

537
Read()){

++current.line;

string w;

for(line>>w; !line.fail(); line>>w){

++current.word;

current.apple = current.apple ||

w.find("alma")!=string::npos;

void Enor::Read()

string str;

getline(f,str,'\n');

if(!f.fail()){

st = norm;

line.clear();

line.str(str);

else st = abnorm;

538
C++ kislexikon

szöveges ifstream x("inp.txt");

állomány char ch;


karakterenkénti
for(x.get(ch); !x.fail(); x.get(ch)){
olvasása
...

ifstream x("inp.txt");

char ch;

x.unsetf(ios::skipws);

for(x >> ch; !x.fail(); x >> ch){

...

szöveges ifstream x("inp.txt");

állomány int n, db = 0;
számainak
for(x >> n; !x.fail(); x >> n){
olvasása
...

szöveges ifstream x("inp.txt");

állományból int szam;


szám-név párok
string nev;
olvasása
for(x>>szam>>nev;

!x.fail();

x>>szam >>nev){

539
...

szöveges ifstream x("inp.txt");

állomány string sor;


soronkénti
for( getline(x,sor,'\n');
olvasása, a
sorokból szám !x.fail();
és név
getline(x,sor,'\n')){
kinyerése
int szam =

atoi(sor.substr( 0, 4).c_str());

string nev = sor.substr( 4,20);

...

540
13. Dinamikus szerkezetű típusok osztályai

Egy típus adatszerkezetén a típus egy értékét reprezentáló elemek


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

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

Implementációs stratégia

Fontos implementációs kérdés, hogy egy összetett adatszerkezetű típust


statikusan vagy dinamikusan reprezentáljunk-e. Mivel egy típus viselkedése a
műveleteinek hatásától függ, könnyen előfordul, hogy két azonos viselkedésű
(azonos típusspecifikációjú) típus közül az egyik statikus, a másik dinamikus
reprezentációjú, ezek kölcsönösen megvalósítják egymást, és nekünk ki kell

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

543
könnyedén alkalmazhatjuk a felsorolókra általánosított programozási
tételeket is. (lásd első kötet)

Egyirányú lista fejelemmel


fejelem
h nil

Egyirányú lista fejelem nélkül

h nil

13-1. ábra. Egyirányú láncolt listák

Jellegzetes, a típusok számítógépes megvalósításánál használatos,


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

544
listaelemeknek is legalább két címrésze van: az egyik a baloldali, a másik a
jobboldali gyerekcsúcsot leíró listaelem címe.

nil nil nil nil nil nil nil nil

13-2. ábra. Jellegzetes láncolt reprezentációk:

egy ciklikus kétirányú láncolt lista és egy bináris fa

Nyelvi elemek

A programozási nyelvek különféle mértékben támogatják, hogy a


programozó egyénileg vezérelje egy adat számára történő memóriafoglalást
illetve annak felszabadítását. A C++ nyelv a C-s hagyományoknak
megfelelően lehetőséget biztosít a közvetlen memóriafoglalásra és
felszabadításra. A lefoglalt memóriaterület címét egy pointerváltozóban
lehet tárolni. Más nyelvek (Java, C#) megpróbálják elfedni a
memóriakezelést, pointerváltozók helyett bevezetik a referencia-típusú
változók fogalmát, a lefoglalt, de már nem használt memóriafoglalások
megszüntetését pedig egy automatikus felszabadító mechanizmusra

545
(garbage collector) bízzák. A továbbiakban a C++ nyelvi lehetőségeit
tárgyaljuk.
A pointerváltozó egy közönséges változó, amely egy olyan
memóriaterület címét veheti fel értékül, ahol a pointerváltozó típusának
megfelelő értéket lehet tárolni. A pointerváltozóban tárolt memóriacím
számára automatikusan jön létre helyfoglalás, de azt a területet, amelyre ez
a cím mutat (amelynek címét majd a pointerváltozó őrzi) a programozónak
külön utasítással (new) kell lefoglalnia a dinamikus memóriából (heap), és ha
már nincs rá szükség, felszabadítania (delete). A pointerváltozó segítségével
közvetett módon tudunk hivatkozni egy általunk lefoglalt memóriaterületen
tárolt értékre, amelyet egy név nélküli változó értékének tekinthetünk.

pointerváltozó memória
regisztráció
név típus cím

cím

érték

13-3. ábra. Pointerváltozó memóriafoglalása

Az int *p deklaráció például egy olyan p pointerváltozót vezet be,


amelyben tárolt címen egy egész típusú értéket helyezhetünk el, de csak az
után, hogy lefoglaltuk számára a szükséges memóriaterületet. A p = new
int értékadás végzi el a helyfoglalást, amely végrehajtása során kijelölődik a
dinamikus memóriában egy egész értéket tárolni képes memóriaterület
(több bájt), és ennek címe a p pointerváltozóba kerül. Ezután és csak ezután
a *p kifejezéssel hivatkozhatunk a lefoglalt területen tárolt egész számra. A
*p=12 vagy *p=*p+1 mind-mind értelmes értékadások erre a dinamikusan
létrehozott, egész típusú, de név nélküli változóra. Ha már nincs szükség a
dinamikus változóra, akkor a delete p utasítással megszüntethetjük a

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

547
pointerváltozót) tömbként használhatjuk, azaz utána írva az indexelő
operátort hivatkozhatunk a pointerváltozóban megadott cím után
elhelyezkedő valahányadik elemre.
int n;

cin >> n;

int* v = new int[n];

verem memória (STACK) dinamikus memória (HEAP)

v int* cím

cím 0 1 2 3

13-4. ábra. Dinamikus helyfoglalású tömb a memóriában

A lefoglalt terület felszabadítása sem automatikus, azt a


felhasználónak kell egy külön utasítással (delete[] v) kezdeményezni.
Hangsúlyozzuk, hogy a dinamikus helyfoglalású tömbök esetében,
akárcsak az automatikus helyfoglalásúaknál, a helyfoglalás után a tömb
méretén már nem lehet változtatni. Természetesen nincs akadálya annak,
hogy egy futás közben dinamikusan változó méretű tömböt készítsünk (bár
elméletben ezt már nem nevezhetjük tömbnek, hiszen annak egyik
alaptulajdonsága, hogy a mérete állandó), ha méretváltozás esetén új
memóriaterületet foglalunk le, ahová átmásoljuk a tömb eddig tárolt
elemeit, majd a régi területet felszabadítjuk. (Tulajdonképpen a vector<>
egy ilyen jellegű tömb, csak elrejti előlünk a dinamikus memóriakezeléssel
járó nehézségeket.)

548
Többdimenziós tömbök, például egy mátrix esetén az automatikus
helyfoglalással az elemek sorfolytonosan egymás után kerülnek elhelyezésre
a memóriában. A dinamikus helyfoglalás esetén ez nem egészen van így. A
folyamat egyrészt két lépcsőben történik, másrészt a mátrix egyes sorai
külön-külön kerülnek elhelyezésre a dinamikus memóriában. Ezek egyben
tartásához először le kell foglalni külön egy egydimenziós tömböt a sorok
memóriacímeinek tárolására – ez tehát egy pointertömb lesz –, majd külö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.

verem memória (STACK) dinamikus memória (HEAP)

v int** cím cím cím cím

0 1 2 3

cím 10 11 12 13

20 21 22 23

13-5. ábra. Dinamikus helyfoglalású mátrix a memóriában

Fenti konstrukció lehetőség ad eltérő elemszámú sorok létrehozására


is. Az ilyen kétdimenziós tömböt szokták kesztyű mátrixnak nevezni.
int** w;

w= new int*[3];

for(int i=0; i<3; ++i) w[i]= new int[4];

A dinamikus helyfoglalású tömbök műveletei megegyeznek az


automatikus helyfoglalású tömbökével. Érdekesség, hogy egy mátrixelemre
pointeraritmetikai kifejezéssel is hivatkozhatnánk. Például a w[i][j] helyett
használhatjuk a *(*(w+i)+j) kifejezést. Ennél hasznosabb lehetőség, hogy

549
a w[i] a mátrix i-edik sorát, mint egydimenziós dinamikusan lefoglalt
tömböt azonosítja.
Súlyos hiba, és még futási időben is rejtve maradhat egy ideig, ha egy
dinamikus helyfoglalású tömbnek olyankor hivatkozunk az elemeire, amikor
azok még nincsenek lefoglalva vagy már fel lettek szabadítva.
A dinamikus helyfoglalású tömb felszabadításáról a programozónak
kell gondoskodni. Kétdimenziós tömbök esetén az elemek felszabadítása is
két lépcsőben történik, csak a lefoglalással ellentétes sorrendben.
for(int i=0; i<3; ++i) delete[] w[i];

delete[] w;

A fentiek mintájára kettőnél több dimenziós dinamikus helyfoglalású


tömbök is készíthetők.
Egy egyirányú láncolt lista listaelemeinek típusát (ami egy rekord) C++
nyelven a struct segítségével írhatjuk le. Az alábbi példában a listaelem
tartalma egész szám lesz, mutató része pedig egy listaelemre mutató pointer.
A struct – az osztályokhoz hasonlóan – konstruktorral is rendelkezhet. (A
struct tagjai – szemben az osztályokkal – hivatalból publikusak.) A NULL a
sehová sem mutató nil pointerértéket reprezentálja.
struct Node {

int value;

Node *next;

Node(int i=0, Node *q=NULL):

value(i), next(q){}

};

Az alábbi három utasítás a konstruktor paraméterváltozóinak


alapértelmezése miatt egyenértékű: olyan listaelemet hoznak létre, amely a
0 egész számot és a nil címet tartalmazza, magának a listaelemnek címe
pedig a p pointerváltozóba kerül.

550
Node *p = new Node();

Node *p = new Node(0);

Node *p = new Node(0, NULL);

A láncolt listákon végezhető egyik alapművelet egy új listaelem


beszúrása illetve. A beszúrás az új elem létrehozásából, értékének
kitöltéséből és a láncba történő befűzéséből áll. Egy fejelemes lista esetén
mindig egy már létező listaelem mögé tudjuk beszúrni az listaelemet, ezért
ha mondjuk egy u pointerváltozó erre a már létező listaelemre mutat (annak
címét tartalmazza), akkor az alábbi kód végzi a 23 értéket tartalmazó
listaelem beszúrását:
Node *p = new Node(23,u->next);

u->next = p;

Fejelem nélküli lista esetében előfordulhat, hogy a legelső listaelem


elé kell egy új listaelemet beszúrni. Ezt az előzőtől némileg eltérő kód végzi
el, ahol feltesszük, hogy a legelső listaelem címét a h pointer őrzi:
h = new Node(23,h->next);

A másik alapművelet egy listaelemnek a törlése egy láncolt listából.


Ehhez először ki kell fűzni ezt az elemet, majd törölhetjük a dinamikus
memóriából. Egy listaelemet a delete p utasítás törli, ha a p pointer a
listaelemre mutat. Tegyük fel, hogy az u pointerváltozó mutatja azt a
listaelemet, amely utáni listaelemet – ha van olyan egyáltalán – kell
kitörölnünk:
Node *p = u->next;

if(p != NULL){

u->next = p->next;

delete p;

551
}

Fejelem nélküli láncolt lista esetében szóba jöhet a legelső listaelem


törlése is, amelyre a h pointerváltozó mutat:
if(h != NULL){

Node *p = h;

h = h->next;

delete p;

A fentiek alapján már könnyen felépíthető illetve lebontható egy teljes


láncolt lista.
Az alábbi kód egy n elemű láncolt lista felépítését mutatja, ahol az
elemek rendre az 1-től n-ig terjedő egész számokat kapják meg értékül. A
lista elejére a h pointer mutat majd, az u és a p segédpointerek.

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

} }

552
A láncolt lista lebontása egyformán történik a fejelemes és fejelem
nélküli változatokra. Ha a h pointer a lista legelső elemére mutat, azaz annak
címét tartalmazza, akkor
while(h != NULL){

Node *p = h;

h = p->next;

delete p;

Fontos tevékenység egy láncolt lista bejárása. Ilyenkor a felsoroló


objektum egy listaelem címét tartalmazó pointerváltozó, amely a bejárás
során mindig az aktuális listaelemre mutat, így annak értéke kiolvasható
(Current()). Kezdetben a lista első elemére állítjuk a felsorolót (First()), a
rákövetkező listaelem címét az aktuális listaelemből olvashatjuk ki (Next()), a
felsorolás végét pedig az jelzi, ha a felsoroló a nil értéket veszi fel (End()),
amelyet lista legutolsó listaeleme tárol következő címként. Az így
megvalósított bejárás segítségével a tanult programozási tételek könnyen
alkalmazhatóak a láncolt listában tárolt értékek feldolgozására. A fejelemes
illetve fejelem nélküli esetek csak a First() művelet megvalósításában térnek
el.

Fejelemmel Fejelem nélkül


First() ~ p = h->next ~ p = h
End() ~ NULL == p ~ NULL == p
Current() ~ p->value ~ p->value
Next() ~ p = p->next ~ p = p->next

13-6. ábra. Egyirányú lista felsoroló műveletei

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

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

555
33. Feladat: Verem

Olvassunk be a szabványos bementről egész számokat, és írjuk ki őket


fordított sorrendben a szabványos kimenetre! A megoldáshoz használjunk
vermet!

Specifikáció

A feladat lényegében egy sorozat megfordítása.

A = ( cin : ℤ*, cout : ℤ* )


Ef = ( cin =cin’ )
cin'
Uf = ( cout cin ' cin' i 1 )
i 1

Mivel mind a bemeneti, mind a kimeneti sorozat speciális, az egyik a


szabványos be-, a másik a kimenet, ezért az utófeltétel által sugallt
megoldással szemben egy vermet fogunk segédadatként használni. Első
menetben bepakoljuk a verembe a szabványos bemenetről érkező számokat,
majd egy második menetben kiürítjük a szabványos kimenetre a verem
tartalmát. Figyelembe véve a verem alaptulajdonságát, mely szerint a
legutoljára betett elemet adja vissza legelőször (LIFO – last in first out) éppen
a kívánt feladatot oldjuk meg.
A vermet nem kell bemutatni az Olvasónak. Ez az a nevezetes
adatszerkezet, amelyik típusára úgy gondolhatunk, amelynek típusértékei
olyan sorozatok, amelyeknek az elejéhez (tetejéhez) lehet hozzáfűzni újabb
elemet (push), az elejéről lehet egy elemet kivenni (pop) illetve kiolvasni
(top) és meg lehet nézni, hogy a sorozat üres-e (empty). A mi esetünkben
egész számok tárolására alkalmas veremre lesz szükségünk.

s:Stack(ℤ) s.Push(e) s.Pop() e:=s.Top() l:=s.Empty()


v:ℤ* v := <e, v> v := e:=v1 l:= v
<v2,…,v v >

556
Absztrakt program

A megoldó program két szintre tagolódik. A felső szinten létrehozunk egy


Stack típusú objektumot, amelyre meghívhatjuk a Push(), Pop(), Top() és
Empty() műveleteket, de nem törődünk azzal, hogyan kell a vermet
megvalósítani. A verem műveletekkel, valamint a beolvasás és kiírás
műveleteivel könnyen elkészíthető a feladatot megoldó két ciklus
szekvenciája: az első feltölti a vermet, a második kiüríti.
Az alábbi struktogrammban kivételesen a C++ nyelvben megszokott
jelölésekkel hivatkozunk a beolvasás és kiírás műveleteire.

cin >> e

cin.fail()

s.Push (e)

cin >> e

s.Empty()

cout << s.Top ()

s.Pop()

A megoldás alsó szintje a verem típusának megvalósítását tartalmazza.


A verem fizikai megvalósításakor a vermet reprezentáló sorozatot kell
megfelelő módon ábrázolni a memóriában. Két klasszikus megoldást szoktak
erre alkalmazni: az egyik egy egybefüggő tömb segítségével ábrázolja a
sorozat elemeit, a másik egy fejelem nélküli láncolt listában. Az előbbi előnye
a kompakt tárolás, hátránya, hogy a tömb létrehozásakor rögzíteni kell egy
korlátot a verembe helyezendő elemek maximális számára nézve. Az utóbbi
előnye, hogy a verem méretét nem kell előre rögzíteni, hátránya viszont,
hogy a listaelemek a tárolt elem mellett egy memória címet is tartalmaznak,
így a memória igénye ennek a megoldásnak nagyobb. A veremműveletek
futási ideje azonban mindkét esetben konstans.

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

558
Implementálás

A program komponens szerkezete

main.cpp stack.h-stack.cpp

class Stack
• Stack()
• ~Stack()
main() • Push()
• Pop()
• Top()
• Empty()

13-7. ábra. Komponens szerkezet

A main.cpp main függvénye tartalmazza az absztrakt megoldás felső


szintjét. Ez a program közvetlenül hivatkozik a Stack osztály metódusaira,
ezért a main.cpp állományba be kell inklúdolni a stack.h állományt.

Főprogram kódolása

Nem igényel különösebb magyarázatot a feladat megoldását végző két ciklus


szekvenciáját tartalmazó main függvény. Ez a tervezésnél megadott
struktogramm C++ nyelvű kódját tartalmazza. Az absztrakt program első
ciklusának kódolásakor kihasználjuk azt, hogy a cin >> i hibás működése
nemcsak a cin.fail() segítségével kérdezhető le, hanem maga a cin >>
i utasítás ad vissza ilyenkor hamis értéket. Ezért a
while(cin >> i){

...

559
}

kódot is alkalmazhatjuk a
cin >> i;

while(!cin.fail()){

...

cin >> i;

kód helyett.
A függvény első sora tömbös megvalósítású verem esetén lehetne a
kommentként megadott utasítás is. Első esetben egy előre rögzített (most
10) maximális elemszámú verem jön létre, a második – kommentként jelzett
esetben – paraméterként adható meg a verem elemszámára adott felső
korlát. A láncolt listás megvalósítás esetén a kommentben feltüntetett
utasítás nem alkalmazható.

Stack s; // Stack s(100);

int i;

while(cin >> i){

try{ s.Push(i);}

catch (Stack::Exceptions e){

if (Stack::FULLSTACK == e)

cout << "A verem megtelt\n";

560
while(!s.Empty()){

cout << s.Pop() << endl;

Az első ciklusban (a verem feltöltésénél) kivételkezelést találunk.


Tömbös megvalósítás esetén a FULLSTACK kivétel akkor keletkezik, amikor a
tömb már tele van a verembe rakott elemekkel, és egy újabb elemet akarunk
betenni. (Ugyanezt a kivételt kellene lekezelni akkor is, ha a paraméteres
konstruktorral hozunk létre vermet, de a paraméterként megadott méret
olyan nagy, hogy ahhoz már nincs elég memória.) Láncolt listás megvalósítás
esetén is van létjogosultsága ennek a kivételnek, hiszen egy újabb listaelem
létrehozásánál már nem biztos, hogy van elég szabad hely az alkalmazás
számára biztosított dinamikus memóriaterületen.

Verem típus osztálya

A verem-típus megvalósítására tehát kétféle osztályt mutatunk: az egyikben


egy tömb segítségével ábrázoljuk a vermet, a másiknál ehhez egy láncolt
listát fogunk használni.
Az osztályok publikus interfésze mindkét esetben ugyanaz: a
konstruktor és destruktor mellett a szokásos verem-műveleteket kínálják fel.
Még a verem-osztály hibás felhasználás esetén dobott kivételei is azonosak a
két verzióban. Ezeket a publikus Exceptions felsorolt típus tartalmazza. A
verem akkor dob EMPTYSTACK kivételt, ha egy üres veremből ki akarunk
venni egy értéket, vagy egy üres verem tetején levő értéket akarunk
használni (Pop()és Top()műveletek). A FULLSTACK kivételnél vagy egy
tömbös reprezentációjú teli verembe akarunk újabb értéket betenni
(Push() művelet) vagy a dinamikus memóriában végrehajtott
memóriafoglalás nem sikerül (tömbös reprezentáció esetén a
konstruktorban, láncolt listás esetben a Push() műveletben).
Egyetlen apró különbséget mutat a kétféle megvalósítás publikus
része: a tömbös megvalósításhoz két konstruktort is biztosítunk.

561
Verem típus tömbös megvalósítása

Az osztály publikus része a kivételeket, két konstruktort, a destruktort és a


verem műveletek metódusait deklarálja.

class Stack{

public:

enum Exceptions{EMPTYSTACK, FULLSTACK};

Stack();

Stack(int s);

~Stack();

void Push(int e);

int Pop();

int Top() const;

bool Empty()const;

Az osztály rejtett részében adjuk meg a verem maximális méretét


tartalmazó adattagot (size), a verem értékeit tartalmazó dinamikus
helyfoglalású tömböt (vect) és a verem tetejét jelző indexet (top).

private:

Stack(const Stack&);

Stack& operator=(const Stack&);

562
void Allocate(int n);

int size;

int* vect;

int top;

};

A verem reprezentációja tehát már itt is dinamikus (habár az


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

Stack::Stack() { Allocate(10); }

Stack::Stack(int n) { Allocate(n); }

563
void Stack::Allocate(int n)

try{

size = n;

vect = new int[n];

top = -1;

}catch(std::bad_alloc o){throw FULLSTACK;}

Stack::~Stack(){ delete[] vect;}

A Push() művelet megnöveli a top értékét – feltéve, hogy így nem


lépi át a tömb indextartományának felső határát, mert különben FULLSTACK
kivételt dob – és a vect tömb top-adik pozíciójára beírja az új elemet. A
Pop()művelet kiolvassa a vect tömb top-adik pozícióján található elemet,
majd csökkenti a top értékét eggyel – feltéve, hogy a top nem -1, mert
ekkor EMPTYSTACK kivételt dob a metódus. A Top()művelet üres lista
esetén EMPTYSTACK kivételt dob, egyébként a vect tömb top-adik
pozícióján található elemet. Az Empty()művelet akkor ad igaz értéket, ha
top értéke -1, egyébként hamis értékkel tér vissza. A Top() és az
Empty()műveletek konstans metódusok, hiszen működésük során nem
módosul a verem objektum.

void Stack::Push(int e)

if( top+1 == size ) throw FULLSTACK;

vect[++top]=e;

564
int Stack::Pop()

if( -1 == top ) throw EMPTYSTACK;

return vect[top--];

int Stack::Top() const

if( -1 == top ) throw EMPTYSTACK;

return vect[top];

bool Stack::Empty()const

return -1 == top;

Verem típus egyirányú fejelem nélküli láncolt listás megvalósítása

Az osztály publikus része egyetlen részletben különbözik az előző


megoldástól: egyetlen paraméter nélküli konstruktora van. Most sort
kerítünk a másoló konstruktor és az értékadás operátor implementációjára
annak ellenére, hogy a főprogram nem használja ezeket. Ezek
alapértelmezett változatai ugyanis rosszul működnének, ha mégis szükség
lenne rájuk.

class Stack{

public:

enum Exceptions{EMPTYSTACK, FULLSTACK};

565
Stack();

~Stack();

Stack(const Stack&);

Stack& operator=(const Stack&);

void Push(int e);

int Pop();

int Top() const;

bool Empty()const;

Az osztály rejtett részében definiáljuk a listaelem típusát. Ezt követi a


verem reprezentációja, ami nem más, mint a legelső listaelemre mutató
head pointer.

private:

struct Node{

int val;

Node *next;

Node(int e, Node *n) : val(e), next(n){}

};

Node *head;

};

566
A konstruktor egy üres vermet, tehát egy üres láncolt listát inicializál,
ehhez beállítja a head értékét nil-re. A destruktor felszabadítja a vermet
megvalósító lista összes listaelemét:

Stack::Stack(): head(NULL){}

Stack::~Stack()

Node *p;

while(head != NULL){

p = head;

head = head->next;

delete p;

A Push()művelet létrehoz egy új listaelemet, és befűzi azt a lista


elejére. Ha nem lehet újabb listaelemet lefoglalni, akkor egy memó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){

567
throw FULLSTACK;

A Pop() művelet üres lista esetén EMPTYSTACK kivételt dob,


egyébként kifűzi a lista legelső elemét, majd felszabadítja azt, de visszaadja a
kifűzött listaelem értékét a hívása helyére.

int Stack::Pop()

if(NULL == head) throw EMPTYSTACK;

int e = head->val;

Node *p = head;

head = head->next;

delete p;

return e;

A Top()művelet üres lista esetén EMPTYSTACK kivételt dob,


egyébként visszaadja a lista legelső elemében tárolt értéket. Az Empty()
művelet üres lista esetén ad igaz értéket, egyébként hamis értékkel tér
vissza.

int Stack::Top()const

if(NULL == head) throw EMPTYSTACK;

568
return head->val;

bool Stack::Empty()const

return NULL == head;

A másoló konstruktor üres inicializáló verem-objektum esetén egy üres


láncolt listát, egyébként egy új láncolt listát épít fel. Ez utóbbiban
ugyanazokat az értékeket, ugyanolyan sorrendben helyezi el, mint amelyek
lemásolandó verem láncolt listájában vannak, tehát egy lista-másolást végez.

Stack::Stack(const Stack& s)

if(NULL == s.head) head = NULL;

else {

try{

head = new Node(s.head->val,NULL);

}catch(std::bad_alloc o){

throw FULLSTACK;

Node *q = head;

Node *p = s.head->next;

while(p != NULL){

try{ q->next = new Node(p->val,NULL);}

569
catch(std::bad_alloc o){throw FULLSTACK;}

q = q->next;

p = p->next;

Az értékadás operátor – feltéve, hogy az értékadás két oldalán


található verem-objektumok nem azonosak – az alapértelmezett verem
objektum (ez az értékadás baloldali objektuma) által foglalt láncolt lista
törléséből, majd az értékül adandó verem objektum láncolt listájának
lemásolásából áll. Mindez kiegészül a megfelelő visszatérési érték
megadásával.

Stack& Stack::operator=(const Stack& s)

if(&s == this) return *this;

Node *p;

while(head != NULL){

p = head;

head = head->next;

delete p;

if(NULL == s.head) head = NULL;

570
else {

try{ head = new Node(s.head->val,NULL);}

catch(std::bad_alloc o){throw FULLSTACK;}

Node *q = head;

Node *p = s.head->next;

while(p != NULL){

try{ q->next = new Node(p->val,NULL);}

catch(std::bad_alloc o){throw FULLSTACK;}

q = q->next;

p = p->next;

return *this;

Megfigyelhető, hogy az értékadás operátor lényegében a


destruktorban és a másoló konstruktorban leírt kód szekvenciája, amely
kiegészül egy kezdeti vizsgálattal (&s==this) és a végén egy speciális
return *this utasítással.

571
Tesztelés

Az alapfeladat érvényes fekete doboz tesztelése nem igényel túl sok


tesztesetet. Egy általános bemenettel ellenőrizhetjük, hogy a kimenten
fordított sorrendben jelennek-e meg az értékek. Természetesen kipróbáljuk
nulla darab és egyetlen értékből álló bemeneteket is.
Az érvénytelen tesztesetek kivételt eredményeznek. A főprogram nem
ad lehetőséget az EMPTYSTACK kivétel keletkezésére, ezért érdemes erre
egy külön tesztprogramot készíteni.

Stack s;

try{ s.Top();}

catch (Stack::Exceptions e){

if (Stack::EMPTYSTACK == e)

cout << "A verem ures!\n";

A FULLSTACK kivétel könnyen kiváltható, ha a tömbös megvalósítású


verem esetén tíznél több elemet akarunk elhelyezni a veremben. Azt is illik
azonban kimérni, hogy legfeljebb mekkora tömböt lehet létrehozni.

try{

int n;

while(true){

cout << "A verem mérete: "; cin >> n;

Stack s(n);

}catch (Stack::Exceptions e){

572
if (Stack::FULLSTACK == e)

cout << "Nincs elég memória!\n";

Láncolt listás megvalósítású verem esetben is egy speciális (nem


interaktív) főprogramot készítünk ahhoz, hogy a „verem betelt” hibaüzenetet
megkapjuk.

573
Stack s;

int i;

try{

while(true){ s.Push(i++); }

}catch (Stack::Exceptions e){

if (Stack::FULLSTACK == e)

cout << "Nincs elég memória!"

<< "Listaelemek szama:" << i << endl;

Az eredeti főprogram viszont nem alkalmas arra, hogy a Stack osztály


komponens tesztjét elvégezzük. Ehhez célszerű egy menüválasztós
tesztprogramot készíteni, amely lehetővé teszi ennek a komponensnek a
fekete és fehér doboz tesztelését. Célszerű egy olyan metódussal is
kiegészíteni az osztályt, amelyik meg tudja jeleníteni a verembe levő értékek
sorozatát. Ezt végrehajtva minden művelet előtt és után pontos képet
nyerhetünk a művelet működéséről. A fehér doboz teszthez hasznos lehet,
ha ez a kiírás nemcsak a verembe levő elemek sorozatát adja meg, hanem
annak ábrázolásával kapcsolatos egyéb adatokat is (tömbös megvalósításnál
a top adattag értékét, láncolt listás esetben a pointer értékeket).A
műveletek önmagukban véve igen egyszerűek, az Olvasó könnyen kitalálhat
rájuk tesztadatokat. Ezen kívül a komponens teszt a műveletek variációs
tesztjét is tartalmazza. Ilyen variációk lehetnek az alábbiak:
1. Ha üres, vagy bármilyen más veremre többször alkalmazzuk a
Push() műveletet, majd ugyanannyiszor a Pop() műveletet, akkor
vissza kell kapnunk a kiindulási vermet.

2. A Push() vagy a Pop() művelet egymás után nem hajtható végre


akárhányszor ugyanazzal az eredménnyel.

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

575
Teljes program

main.cpp:
#include <iostream>

#include "stack.h"

using namespace std;

int main()

Stack s;

int i;

while(cin >> i){

try{ s.Push(i);}

catch (Stack::Exceptions e){

if (Stack::FULLSTACK == e)

cout << "A verem megtelt\n";

while(!s.Empty()){

cout << s.Pop() << endl;

return 0;

stack.h: (tömbös változat)

576
#ifndef STACK_H

#define STACK_H

class Stack{

public:

enum Exceptions{EMPTYSTACK, FULLSTACK};

Stack();

Stack(int s);

~Stack();

void Push(int e);

int Pop();

int Top() const;

bool Empty()const;

private:

Stack(const Stack&);

Stack& operator=(const Stack&);

void Allocate(int n);

int size;

int* vect;

577
int top;

};

#endif

stack.cpp: (tömbös változat)


#include "stack.h"

#include <memory>

Stack::Stack() { Allocate(10); }

Stack::Stack(int n) { Allocate(n); }

void Stack::Allocate(int n)

try{

size = n;

vect = new int[n];

top = -1;

}catch(std::bad_alloc o){throw FULLSTACK;}

Stack::~Stack()

delete[] vect;

void Stack::Push(int e)

578
{

if( top+1 == size ) throw FULLSTACK;

vect[++top]=e;

int Stack::Pop()

if( -1 == top ) throw EMPTYSTACK;

return vect[top--];

int Stack::Top() const

if( -1 == top ) throw EMPTYSTACK;

return vect[top];

bool Stack::Empty()const

return -1 == top;

stack.h: (láncolt listás változat)


#ifndef STACK_H

#define STACK_H

class Stack{

public:

579
enum Exceptions{EMPTYSTACK, FULLSTACK};

Stack();

~Stack();

Stack(const Stack&);

Stack& operator=(const Stack&);

void Push(int e);

int Pop();

int Top() const;

bool Empty()const;

private:

struct Node{

int val;

Node *next;

Node(int e, Node *n) : val(e), next(n){}

};

Node *head;

};

#endif

580
581
stack.cpp: (láncolt listás változat)
#include "stack.h"

#include <memory>

using namespace std;

Stack::Stack(): head(NULL){}

Stack::~Stack()

Node *p;

while(head != NULL){

p = head;

head = head->next;

delete p;

void Stack::Push(int e)

try{ head = new Node(e,head);}

catch(std::bad_alloc o){ throw FULLSTACK;}

int Stack::Pop()

if(NULL == head) throw EMPTYSTACK;

int e = head->val;

582
Node *p = head;

head = head->next;

delete p;

return e;

int Stack::Top()const

if(NULL == head) throw EMPTYSTACK;

return head->val;

bool Stack::Empty()const

return NULL == head;

583
Stack::Stack(const Stack& s)

if(NULL == s.head) head = NULL;

else {

try{

head = new Node(s.head->val,NULL);

}catch(std::bad_alloc o){

throw FULLSTACK;

Node *q = head;

Node *p = s.head->next;

while(p != NULL){

try{ q->next = new Node(p->val,NULL);}

catch(std::bad_alloc o){throw FULLSTACK;}

q = q->next;

p = p->next;

Stack& Stack::operator=(const Stack& s)

if(&s == this) return *this;

584
Node *p;

while(head != NULL){

p = head;

head = head->next;

delete p;

if(NULL == s.head) head = NULL;

else {

try{ head = new Node(s.head->val,NULL);}

catch(std::bad_alloc o){throw FULLSTACK;}

Node *q = head;

Node *p = s.head->next;

while(p != NULL){

try{ q->next = new Node(p->val,NULL);}

catch(std::bad_alloc o){throw FULLSTACK;}

q = q->next;

p = p->next;

return *this;

585
34. Feladat: Kettős sor

A szabványos bemenetről érkező egész számokat szortírozzuk és írjuk ki a


szabványos kimenetre úgy, hogy először a negatívokat, majd azt követően a
többit jelenítjük meg! Ezen kívül minden kiírt szám mellé odaírjuk azt is, hogy
az hányszor szerepelt a bemeneti értékek között! A feladat megoldásához
használjunk egy kettős sort!

Specifikáció

A feladat megoldását két szakaszra bontjuk. Először a bemenő értékek


sorozatát alakítjuk át úgy, hogy az elején legyenek a negatív számok, a végén
pedig a többi. (Kényelmesebb a specifikációban a bemenő sorozatot egy
felsoroló segítségével elérni: ezt jelöli a cin.) Az első szakasz eredményeként
létrejött t sorozat elemeit a második szakaszban bejárjuk, és minden elemére
megszámoljuk, hogy az hányszor szerepel ebben a sorozatban. Ehhez a t
sorozat kétszintű bejárására van szükség, hiszen az elemek bejárása közben
minden elemre egy újabb bejárással kell a számlálást megvalósítani.

A = ( cin : enor(ℤ), cout : ℤ* )


Ef = ( cin =cin’ )
Uf = ( t ( e ) ( e )
e cin' e cin'
e 0 e 0
cout (e, 1) )
e t d t
d e

A feladat megoldásában szereplő t sorozatot egy speciális tárolóként


képzeljük el. Ennek, túl azon, hogy egész számok tárolására képes, vagy az
elejére vagy a végére illeszthető be könnyen újabb érték. Így egyszerűen
megoldhatjuk azt, hogy a bementről érkező negatív egészeket előre, a többit
a sorozat végére fűzzük.
Egy olyan tárolót, amelyet egy sorozat reprezentál, és amelynek a
végeihez lehet elemeket hozzáfűzni, és csak onnan lehet elemet kivenni,
kettős sornak nevezzük. Erre az alábbi műveleteket vezetjük be:

586
Loext() : egy elemi érték berakása a sor elejére
Lopop() : egy elemi érték levétele a sor elejéről
Hiext() : egy elemi érték berakása a sor végére
Hipop() : egy elemi érték levétele a sor végéről

b:BiQueue(ℤ) b.Loext(e) b.Lopop() b.Hipop()


b.Hiext(e)
t : ℤ* t := <e, t> t := <t2,…,t t > t := <t1,…, t t –1>
t := <t, e> e := t1 e := t t

A kettős sor elemeinek bejárásánál szükségünk lesz egy olyan felsoroló


objektumra, amely rendelkezik a szokásos bejáró műveletekkel. Esetünkben
ez az alábbiakat jelenti:

First() : a kettős sor elejére áll


Next() : a kettős sor következő elemére áll
End() : jelzi, hogy a kettős sor végére értünk-e
Current() : az aktuális értéket adja vissza

A felsoroló objektumot a kettős sor speciális művelete, a


CreateEnumerator() hozza majd létre.
Felvethető az a kérdés, hogy miért akarunk külön felsoroló
objektumokat létrehozni, miért nem elég, ha a bejáró műveleteket
közvetlenül a kettős sor műveletei közé vesszük fel. Ha így tennénk, akkor
egyszerre csak egy bejárást tudnánk egy soron végezni. Márpedig a mi
feladatunk az, hogy a sor elemeinek kiíratásához indított bejárás közben
minden elemnél megálljunk, és a kívánt számlálás elvégzéséhez egy újabb
bejárást is indítsunk. Annak pedig, hogy egy tárolón egy időben több bejárást
indíthassunk, az a feltétele, hogy tetszőleges számú felsoroló objektumot
lehessen létrehozni és használni.

587
Absztrakt program

A megoldó programnak két szintje lesz. A felső szinten létrehozunk egy


kettős sort, és az előírt módon feltöltjük a Loext() és Hiext() műveleteinek
segítségével. Ezután létrehozunk a kettős sorhoz több felsoroló objektumot
és azok First(), Next(), End() és Current() műveleteire támaszkodva
elvégezzük az elemeknek a számlálásokat is magába foglaló kiíratását.
A megoldás tehát két ciklus szekvenciája lesz. Az elsőben a C++
nyelvben megszokott jelölésekkel hivatkozunk a beolvasásra. A 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

588
it1.Next() it2.Next()

A megoldás alsó szintje a kettős sor típusának és a kettős sor


felsorolója típusának megvalósítását tartalmazza. Mindkettőre egy-egy
osztályt hozunk létre.
A kettős sort egy fejelem nélküli kétirányú láncolt listával fogjuk
reprezentálni. Két pointerrel hivatkozunk majd erre a listára:
first: lista első elemére mutat (üres lista esetén nil)
last: lista utolsó elemére mutat (üres lista esetén nil)

A kettős sor műveleteit a láncolt lista elejére és végére történő


beszúrás, és onnan való törlés implementálja. Ezen műveletek mellé egy
speciális metódust is felveszünk: a CreateEnumerator() segítségével tudunk
majd a kettős sorhoz egy új felsoroló objektumot létrehozni.
A felsoroló objektumot két pointerrel reprezentáljuk. Az egyik arra a
kettős sorra mutat, amelyik elemeit felsoroljuk, a másik a kettős sort
reprezentáló láncolt lista azon listaelemére mutat majd, amelyik a felsorolás
során érintett aktuális elem.

Implementálás

A program komponens szerkezete

589
main.cpp biqueue.h-biqueue.cpp biqueue.h

class BiQueue class Enumerator


• BiQueue() • Enumerator()
• ~BiQueue() • ~Enumerator()
• Loext() • First()
main() • Lopop() • Next()
• Hiext() • Current()
• Hipop() • End()
• CreateEnumerator()

13-8. ábra. Komponens szerkezet

A main függvény tartalmazza az absztrakt megoldás felső szintjét. Ez a


program közvetlenül hivatkozik a BiQueue osztályra és annak belső
osztályaként definiált Enumerator osztályra, ezért a main.cpp állományba
be kell inklúdolni a biqueue.h állományt.

Főprogram kódolása

A főprogramban létrehozunk egy üres kettős sort, majd a szabványos


bementről érkező számokat előjelüktől függően belerakjuk a sorba. A
kódolásnál ismét kihasználjuk, hogy a cin >> i hibás működés esetén
hamis értéket ad vissza.

BiQueue x;

int i;

while(cin >> i){

if (i>0) x.Hiext(i);

else x.Loext(i);

590
}

A beolvasás után bejárjuk a sort, és kiírjuk az elemeit a szabványos


kimenetre, közben minden elemre megszámoljuk, hányszor van benne a
sorban. Ehhez egyidejűleg két felsorolót használunk. A BiQueue osztályba
ágyazott Enumerator osztályra a BiQueue::Enumerator-ral
hivatkozhatunk.

BiQueue::Enumerator it1 = x.CreateEnumerator();

for(it1.First(); !it1.End(); it1.Next()){

i = it1.Current();

int s = 0;

BiQueue::Enumerator it2

= x.CreateEnumerator ();

for(it2.First(); !it2.End(); it2.Next()){

if (it2.Current() == i) ++s;

cout << i << " előfordulásainak száma: "

<< s << endl;

return 0;

A kettős sor osztálya

Az Exceptions felsorolt típus azt az értéket tartalmazza, amelyet a


kettős sor hibás felhasználás esetén kivételként dob. Jelen esetben ez akkor

591
következik be, ha egy üres sorból ki akarunk venni egy értéket (ld. Lopop()
és Hipop() műveleteknél).

class BiQueue{

public:

enum Exceptions{EMPTYSEQ};

BiQueue():

first(NULL),last(NULL){}

~BiQueue();

void Loext(int e);

int Lopop();

void Hiext(int e);

int Hipop();

BiQueue(const BiQueue&);

BiQueue& operator=(const BiQueue&);

A konstruktort inline módon definiáltuk. Ez egy üres kettős sort, azaz


egy üres láncolt listát hoz létre úgy, hogy a két privát pointert NULL-ra állítja.
Mind a másoló konstruktort, mind az értékadás operátort újradefiniáljuk.

private:

struct Node{

int val;

Node *next;

592
Node *prev;

Node(int c, Node *n, Node *p):

val(c), next(n), prev(p){};

};

Node *first;

Node *last;

A privát részben adjuk meg a listaelemek típusát. Ez egy három részből


(érték és két pointer) álló struktúra (Node), amelynek konstruktorával
hozhatunk létre egy új listaelemet. E struktúra ismeretében definiáljuk a
láncolt lista legelső illetve legutolsó listaelemére mutató first és last
pointereket. Ez a két pointer lényegében a kettős sor reprezentációja.
Ezzel a BiQueue osztály definícióját még nem fejeztük be. Hiányzik a
CreateEnumerator() metódus deklarációja, de ehhez előbb szükség van
az Enumerator osztály definíciójára.

Felsoroló a kettős sorhoz

Egy kettős sor felsorolását megvalósító osztályt (class Enumerator) a


BiQueue osztály beágyazott publikus osztályaként definiáljuk. Ez egyrészt
egyértelművé teszi, hogy itt a BiQueue osztály felsorolójáról lesz szó,
másrészt az Enumerator osztály metódusai hivatkozhatnak a BiQueue
osztály privát elemeire is.
Egy felsorolót elsősorban az a current pointer reprezentálja, amelyet
a kettős sor láncolt listáján tudunk végigvezetni és a bejárás során mindig az
aktuális listaelemre mutat. Ezen kívül a felsoroló magára a bejárni kívánt
kettős sorra is hivatkozik egy pointer segítségével (bq). Az Enumerator
osztály metódusai: First(), Next(), Current(), End(). Ezeknek a
műveleteknek az implementációja igen egyszerű (éppen ezért inline módon
adjuk meg), lényegében a kettős sort reprezentáló láncolt listával és a
current pointerrel operálnak. A First() ráállítja a current pointert a
lista első elemére, a Next() a következőre, az End() akkor ad igazat, ha a

593
current értéke már NULL (azaz lefutott a listáról), a Current() pedig a
current pointer által mutatott listaelem értékét adja vissza.

class Enumerator{

public:

Enumerator(BiQueue *p): bq(p),current(NULL){};

int Current()const {return current->val;}

void First() {current = bq->first;}

bool End() const {return NULL == current;}

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;

594
return it;}

Enumerator& operator++(){ // ++it

current = current->next;

return *this;}

Egy BiQueue::Enumerator it(&x) utasítás segítségével


hozhatunk létre egy it nevű felsorolót az x kettős sorhoz. Ezt a műveletet a
kettős sor korábban már beígért CreateEnumerator() metódusával is
elvégezhetjük. Térjünk tehát vissza a kettős sor osztályának definíciójához.

A kétirányú sor osztálydefiníciójának folytatása

Most már felvehetjük a BiQueue osztályba a CreateEnumerator()


publikus metódust, amelynek implementációját inline módon adjuk meg.

Enumerator CreateEnumerator()

{ return Enumerator(this); }

Az BiQueue osztály definíciója ezzel el is készült. Hátra van még a


korábban deklarált műveleteknek a megvalósítása.
A destruktor felszabadítja a kettős sort ábrázoló láncolt listát. Itt is
látható, hogy míg a konstruktor egyáltalán nem végez memóriafoglalást,
addig a destruktor számos listaelemet felszabadíthat, hiszen a kettős sor
használata során a Loext() és Hiext() műveletek dinamikusan hozzák
létre a listaelemeket.

BiQueue::~BiQueue(){

Node *p, *q;

q = first;

595
while( q != NULL){

p = q;

q = q->next;

delete p;

A másoló konstruktor egy p pointert vezet végig a lemásolandó kettős


soron (feltéve, hogy az nem üres), és az új kettős sor listájának felépítéséhez
egy q pointert használ.

BiQueue::BiQueue(const BiQueue &b){

if(NULL == b.first){

first = last = NULL;

}else{

Node *q = new Node(b.first->val,NULL,NULL);

first = q;

for(Node *p=b.first->next;

p != NULL;p=p->next){

q = new Node(p->val,NULL,q);

q->prev->next = q;

last = q;

596
Az értékadás operátort az alábbi séma mintájára definiáljuk.
BiQueue& BiQueue::operator=(const BiQueue &s){

if(&s == this) return *this;

// destruktor

// másoló konstruktor

return *this;

Miután megvizsgálja, hogy az alapértelmezés szerinti (értékadás


baloldali) objektuma különbözik-e az értékül adandó objektumtól, előbb a
destruktornak megfelelően felszabadítja az alapértelmezés szerinti objektum
memóriafoglalásait, majd a másoló konstuktorhoz hasonlóan létrehozza az új
kettős sort.

BiQueue& BiQueue::operator=(const BiQueue &s){

if(&s == this) return *this;

Node *p = first;

while(p != NULL){

Node *q = p->next;

delete p;

p = q;

if(NULL == s.first){

first = last = NULL;

}else{

597
Node *q = new Node(s.first->val,NULL,NULL);

first = q;

for(Node *p=s.first->next;

p != NULL;p=p->next){

q = new Node(p->val,NULL,q);

q->prev->next = q;

last = q;

return *this;

A Loext() illetve a Hiext() művelet létrehoz egy új listaelemet,


kitölti annak értékét, és befűzi a lista elejére illetve a végére, és ennek
megfelelően állítjuk a first vagy a last pointert. Amikor a legelső elemet
fűzzük be az üres listába, akkor mindkét esetben mindkét pointert állítani
kell.

void BiQueue::Loext(int e){

Node *p = new Node(e,first,NULL);

if(first != NULL) first->prev = p;

first = p;

if(NULL == last) last = p;

void BiQueue::Hiext(int e){

598
Node *p = new Node(e,NULL,last);

if(last != NULL) last->next = p;

last = p;

if(NULL == first) first = p;

A Lopop() művelet EMPTYSEQ kivételt dob, ha a lista üres, egyébként


kifűzi a lista legelső elemét, a benne tárolt értéket elmenti, és a listaelemet
felszabadítja. Ha a lista eredetileg egyelemű volt, akkor az utolsó elemre
mutató last pointert NULL-ra kell állítani. A Hipop() a Lopop() duálisa.

int BiQueue::Lopop(){

if(NULL == first) throw EMPTYSEQ;

int e = first->val;

Node *p = first;

first = first->next;

delete p;

if(first != NULL) first->prev = NULL;

else last = NULL;

return e;

int BiQueue::Hipop(){

if(NULL == last)throw EMPTYSEQ;

int e = last->val;

Node *p = last;

599
last = last->prev;

delete p;

if(last != NULL) last->next = NULL;

else first = NULL;

return e;

Elem törlése bejárás közben

Egy problémával érdemes még foglalkoznunk annak ellenére, hogy a fenti


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

600
felsorolókat, a listaelemeket pedig ki kell egészíteni egy törlést
jelző mezővel. Egy törlés újbóli kísérletét a felsorolók Next()
műveletének végrehajtásához köthetjük.

Tesztelés

Az alapfeladat érvényes fekete doboz tesztelését kipróbálhatjuk nulla darab


bemenettel, majd egy darabbal, majd csupa különböző értékkel (legyenek
köztük negatív és nem negatív értékek is), csupa azonos értékkel, végül egy
általános esettel.
Az érvénytelen tesztesetek a kivétel dobások kikényszerítésére
irányulnak. A főprogram azonban nem ad lehetőséget az EMPTYSTACK
kivétel dobására, hiszen nem törlünk. Érvénytelen teszteset az is, amikor
valamelyik new utasítás bad_alloc kivételt dob. Ennek kikényszerítésére
sem alkalmas a főprogramunk, az előző feladatnál látott tesztprogramot
kellene most is elkészíteni.
A BiQueue osztály komponens tesztjéhez most is egy menüválasztós
tesztprogramot kell készíteni. A műveletek variációs tesztjéhez a teljesség
igénye nélkül néhány példa:
1. Ha üres, vagy bármilyen más sorra többször alkalmazzuk a Hiext()
műveletet, majd ugyanannyiszor a Lopop() műveletet, akkor
visszakapjuk a kiindulási sort.

2. A Hiext(), Loext(), Hipop(), Lopop() művelet bármelyike


egymás után nem hajtható végre akárhányszor ugyanazzal az
eredménnyel.

3. Kezdetben üres sorra végrehajtott Hiext(1), Hiext(2),


First(), Next(), Hipop() után „törlés bejárás közben” kivétel
keletkezik.

601
Teljes program

main.cpp:
#include <iostream>

#include "biqueue.h"

using namespace std;

int main()

BiQueue x;

int i;

while(cin >> i){

if (i>0) x.Hiext(i);

else x.Loext(i);

BiQueue:: Enumerator it1 = x.CreateEnumerator();

for(it1.First(); !it1.End(); it1.Next()){

i = it1.Current();

int s = 0;

BiQueue:: Enumerator it2

= x.CreateEnumerator ();

for(it2.First(); !it2.End(); it2.Next()){

if (it2.Current() == i) ++s;

602
}

cout << i << " előfordulásainak száma: "

<< s << endl;

return 0;

603
biqueue.h:
#ifndef BIQUEUE_H

#define BIQUEUE_H

#include <memory>

class BiQueue{

public:

enum Exceptions{EMPTYSEQ, UNDERTRAVERSAL};

BiQueue():

first(NULL),last(NULL),enumeratorCount (0){}

BiQueue(const BiQueue&);

BiQueue& operator=(const BiQueue&);

~BiQueue();

void Loext(int e);

int Lopop();

void Hiext(int e);

int Hipop();

private:

struct Node{

int val;

Node *next;

Node *prev;

Node(int c, Node *n, Node *p):

val(c), next(n), prev(p){};

604
};

Node *first;

Node *last;

int enumeratorCount;

public:

class Enumerator{

public:

Enumerator(BiQueue *p):bq(p),current(NULL)

{++(bq->enumeratorCount);}

~Enumerator(){--(bq->enumeratorCount);}

int Current()const {return current->val;}

void First() {current = bq->first;}

bool End() const {return NULL == current;}

void Next() {current = current->next;}

private:

BiQueue *bq;

Node *current;

};

Enumerator CreateEnumerator()

{return Enumerator(this);}

};

#endif

biqueue.cpp:

605
#include "biqueue.h"

using namespace std;

BiQueue::~BiQueue(){

Node *p, *q;

q = first;

while( q != NULL){

p = q;

q = q->next;

delete p;

BiQueue::BiQueue(const BiQueue &s){

if(NULL == s.first)first = last = NULL;

else{

Node *q = new Node(s.first->val,NULL,NULL);

first = q;

for(Node *p=s.first->next;

p != NULL;p=p->next){

q = new Node(p->val,NULL,q);

q->prev->next = q;

last = q;

606
}

BiQueue& BiQueue::operator=(const BiQueue &s){

if(&s == this) return *this;

Node *p = first;

while(p != NULL){

Node *q = p->next;

delete p;

p = q;

if(NULL == s.first) first = last = NULL;

else{

Node *q = new Node(s.first->val,NULL,NULL);

first = q;

for(Node *p=s.first->next;

p != NULL;p=p->next){

q = new Node(p->val,NULL,q);

q->prev->next = q;

last = q;

return *this;

607
void BiQueue::Loext(int e){

Node *p = new Node(e,first,NULL);

if(first != NULL) first->prev = p;

first = p;

if(NULL == last) last = p;

int BiQueue::Lopop(){

if(enumeratorCount != 0) throw UNDERTRAVERSAL;

if(NULL == first) throw EMPTYSEQ;

int e = first->val;

Node *p = first;

first = first->next;

delete p;

if(first != NULL) first->prev = NULL;

else last = NULL;

return e;

void BiQueue::Hiext(int e){

Node *p = new Node(e,NULL,last);

if(last != NULL) last->next = p;

last = p;

if(NULL == first) first = p;

int BiQueue::Hipop(){

608
if(enumeratorCount != 0) throw UNDERTRAVERSAL;

if(NULL == last)throw EMPTYSEQ;

int e = last->val;

Node *p = last;

last = last->prev;

delete p;

if(last != NULL) last->next = NULL;

else first = NULL;

return e;

609
C++ kislexikon

pointer int* p;

dinamikus int* p = new int;

helyfoglalás int *t = new int[10];


és törlés
delete p;

delete[] t;

listaelem struct Node {

definiálása int value;

Node *next;

Node(int i=0, Node *q=NULL)

:value(i), next(q){}

};

listaelem Node *p = new Node();

létrehozása 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 // Node *u egy létező listaelem címével

listaelem Node *p = u->next;


mögül if(p!=NULL){

u->next = p->next;

delete p;

610
}

lista Fejelemes Fejelem nélküli


felépítése 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 Node *p

= new Node(i); = new Node(i);

u->next=p; u->next=p;

u = p; u = p;

} }

lista while(h!=NULL){

lebontása Node *p = h;

h = p->next;

delete p;

értékadás O& O::operator=(const O &s){

operátor if(&s == this) return *this;

// destruktor törzse

// másoló konstruktor törzse

return *this;

611
14. Objektum-orientált kód-újrafelhasználási technikák

Egy program költségét több szempont alapján határozhatjuk meg: ilyenek a


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

612
akkor először ezeket a sablon-paramétereket kell konkrét elemekkel
helyettesíteni, azaz az osztály-sablonból létre kell hoznunk, példányosítanunk
kell egy konkrét osztályt.
Mindkét nyelvi eszköz azt támogatja, hogy közös tulajdonsággal
rendelkező osztályok használata esetén, az osztályok hasonló elemeit csak
egyszer kelljen megadni, leírni, és azokat újra és újra felhasználni.

Implementációs stratégia

Az, hogy a programunkban használt osztályok előállításához kell-e


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

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

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

2
Megjegyezzük, hogy az absztrakt osztály és az absztrakt típus fogalma
között nincs szoros összefüggés: az absztrakt osztály nem az absztrakt típus
megvalósítása. Az absztrakt típust egy adat jellemzésére szolgáló
fogalomként vezettük be, amelyet részben vagy nem végleges formában
valósítottunk meg. Az absztrakt osztály viszont a kód-újrafelhasználás

615
Az általánosítás és specializálás jól alkalmazható az alternatív típus
megvalósításánál. (Ez az a típusszerkezet, amelyre a programozási nyelvek
általában nem biztosítanak közvetlen nyelvi eszközt ellentétben a 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.

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

617
Nyelvi elemek

A fenti implementációs elveket nyelvi szinten a származtatás és a


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

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

619
lefutni. (A látszat ellenére a tisztán objektum-orientált nyelvek és a C++ nyelv
dinamikus kötése között valójában nincs különbség. A tisztán 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,

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

621
35. Feladat: Túlélési verseny

Különféle élőlények ugyanazon a váltakozó terepekből álló pályán indulnak el


sorban egymás után. Egy lénynek attól függően változik az életereje, hogy
milyen terepen megy át, de közben a terepet is átalakítja. Egészen addig
halad, amíg végig nem ér a pályán vagy el nem fogy az életereje és elpusztul.
Egy terep akkor is átalakul, ha azon a lény elpusztul. Az első lény az eredeti
pályát, a további lények az elöttük levő által átalakított pályát használják.
Adjuk meg a pályán végig jutó, azaz életben maradt lények neveit!
A pályán három féle terep fordulhat elő: fű, homok, mocsár. A lények
három különböző fajta egyikéhez tartozhatnak.
Zöldike: kezdeti életereje 10; füvön az életereje eggyel nő, homokon
kettővel csökken, mocsárban eggyel csökken; a mocsaras terepet fűvé
alakítja, a másik két féle terepet nem változtatja meg.
Buckabogár: kezdeti életereje 15; füvön az ereje kettővel csökken,
homokon hárommal nő, mocsárban néggyel csökken; a füvet homokká, a
mocsarat fűvé alakítja, de a homokot nem változtatja meg.
Tocsogó: kezdeti életereje 20; füvön az életerő kettővel, homokon öttel
csökken, mocsárban hattal nő; a füvet mocsárrá alakítja, a másik két féle
terepet nem változtatja meg.
Minden lénynek van egy neve (sztring), ismert az aktuális életereje
(egész szám) és a fajtája. Egy lény addig él, amíg az életereje pozitív.
A verseny adatait egy szöveges állományból olvassuk be! A fájl első
sora tartalmazza a lények számát, amelyet a lények soronkénti leírása követ.
Ez a fajtát jelölő karakter (Z – zöldike, B – buckabogár, T – tocsogó), amit
szóköz után a lény neve követ. Ezek után következik a pálya leírása. 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ó

622
T szivacs
10 0210201012

623
Specifikáció

A feladat megoldásában központi szerepet játszanak a lények. Attól


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

14-1. ábra. Lények osztálydiagrammja

624
Zöldikék esetében a kezdő életerő: 10, amit a konstruktor állít be. Az
Átalakít() művelet hatását az alábbi táblázat foglalja össze. Ez megkapja
bemenetként az aktuális terepet, és a táblázat megfelelő sora alapján
megváltoztatja az aktuális lény életerejét és visszaadja az új terepet.

terep életerő változás terepváltozás

homok -2 -
fű +1 -
mocsár -1 fű

Buckabogarak esetében a kezdő életerő: 15, és az Átalakít() művelet


hatása:

terep életerő változás terepváltozás

homok +3 -
fű -2 homok
mocsár -4 fű

Tocsogók esetében a kezdő életerő: 20, és az Átalakít() művelet


hatása:

terep életerő változás terepváltozás

homok -5 -
fű -2 mocsár
mocsár +6 -

625
A lények absztrakt osztályát és az abból származtatott speciális lények
osztályait ezek alapján már könnyen elkészíthetjük. A speciális osztályok
konstruktorai meghívják az ősosztály konstruktorát, majd inicializálják az
életerőt. Az Él() és Név() metódusok az ősosztály szintjén implementálhatók.
Az Él() metódus akkor ad igaz értéket, ha az életerő pozitív. A Név() metódus
a név adattag értékét adja vissza. Az Átalakít() metódus az ősosztályban
absztrakt, a konkrét osztályok szintjén kell definiálni a korábban definiált
táblázatok alapján. Ez módosítja az életerőt és megváltoztatja az adott
pályamezőt.
Most pedig specifikálhatjuk a teljes feladatot. A specifikációban meg
kell különböztetni egy-egy lény áthaladta utáni pálya állapotokat. A nulladik
változat a kezdőpálya, az i-edik az i-edik lény mozgása után kialakult pálya.

m n *
A = ( kezdőpálya : ℕ , lények : Lény , 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’
mn
pálya:(ℕ ) pálya[0]=kezdőpálya
i [1..n]: pálya[i]=Eredm(lények[i],pálya[i-1])2
n
túlélők lények [i ].név )
i 1
Eredm (lények[i ], pálya[i 1])1.Él ()
m m
ahol az Eredm: Lény × ℕ Lény × ℕ 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
m
r:[0 .. m] Lény × ℕ
r(0) = (lény,pálya)
Lépés (r ( j 1)) ha r ( j 1) 1 .Él ()
j [1..m]: r ( j)
r ( j 1) ha r ( j 1) 1 .Él ()
A Lépés(r(j-1)) állítja be a j-edik lépés utáni a lény életerejét (az r(j)1 jelöli
ekkor a lényt, amelynek a korábbi állapotát az r(j-1)1 mutatja), valamint az

626
r(j)2 pályát (valójában a pályának csak a j-edik mezője változhat a pálya előző
r(j-1)2 állapotához képest). Ezeket a változásokat a korábban definiált
Átalakít() függvény alapján számíthatjuk ki.
Ez így egy kicsit bonyolultnak tűnik, de objektum orientált szemlélettel
megfogalmazva a rekurzív függvény j-edik lépését az i-edik lényre már sokkal
egyszerűbb felírni: lények[i].Átalakít(pálya[j]), feltéve, ha lények[i].Él().

Absztrakt program

A megoldó programnak két szintje van. A felső szinten a fenti specifikációnak


megfelelő algoritmust írjuk le, az alsó szinten a lények osztályait.
A külső ciklus a lényeket veszi sorra. Minden lényt végig vezet a
pályán, de csak addig, amíg él, közben átalakítja a pályát, és ha túléli a lény a
pályát, akkor kiírja a nevét.

i = 1..n
j:=1

j m lények[i].Él()
lények[i].Átalakít(pálya[j])
j:=j+1
lények[i].Él()

túlélők:=túlélők lények[i].Név() SKIP

627
Implementálás

A programkód a kommenteket nem számítva most is angol nyelvű lesz, de itt


talán célszerű egy kis magyar-angol szótárt is mellékelni a megvalósításhoz.

lény creature átalakít transmute


zöldike greenfinch pálya field
buckabogár sandbug terep ground
tocsogó squelchy fű grass
erő power homok sand
név name mocsár swamp
él alive

A program komponens szerkezete

Az absztrakt algoritmust a main.cpp állományban elhelyezett main


függvényben találjuk. Az osztályok definíciói a creature.h fejállományba,
az Transmute() metódusok implementációi a creature.cpp
forrásállományba kerülnek.

Főprogram kódolása

A main függvény az absztrakt program kódján kívül a lények és a pálya


beolvasását is tartalmazza.
A versenyen résztvevő lényekre történő hivatkozásokat, azaz
Creature* típusú elemeket egy tömbben (vector<Creature*>) tároljuk.
Ha nem a Creature osztály egy objektumát, hanem a Creature osztályból
származtatott osztály egy objektumát hozzuk létre, akkor ennek hivatkozása
(címe) is elhelyezhető ebben a tömbben. Így lényegében egy olyan tömböt
használunk, amelyik vegyesen tárolhat különböző, de a Creature osztályból

628
származtatott osztályú (típusú) elemeket: a tömb egy elemének tehát
alternatív szerkezetű típusa van.

ifstream f("input.txt");

int n;

f >> n;

vector<Creature*> creatures(n);

for(int i=0; i<n; ++i){

char l;

string a;

f >> l >> a;

switch(l){

case 'T' : creatures[i] = new Squelchy(a);

break;

case 'Z' : creatures[i] = new Greenfinch(a);

break;

case 'B' : creatures[i] = new Sandbug(a);

break;

default:;

629
A pálya a pályamezők számkódjait tartalmazó tömbbe (vector<int>)
kerül.

int m;

f >> m;

vector<int> field(m);

for(int j=0; j<m; ++j) f >> palya[j];

A feldolgozást a struktogramm alapján kódoljuk. Figyelembe kell venni,


hogy a creatures tömb a C++ megvalósításban pointereket tárol, ezért
például az i-edik lény által okozott átalakítást itt a creatures[i]-
>Transmute() alakú metódushívással tehetjük meg.

for(int i=0; i<n; ++i){

for(int j=0; creatures[i]->Alive() && j<m; ++j){

creatures[i]->Transmute(palya[j]);

if (creatures[i]->Alive())

cout << creatures[i]->Name() << endl;

A program végén ne felejtsük el felszabadítani a saját


memóriafoglalásainkat.

for(int i=0; i<n; ++i){

delete creatures[i];

630
}

Creature osztály

Ez a specifikációnak megfelelő absztrakt osztály. Absztrakt voltára két dolog


is felhívja a figyelmet: a konstruktora nem publikus és a Transmute()
metódusa nincs implementálva. Ennél fogva ilyen típusú objektumot nem
lehet létrehozni.

class Creature {

protected:

std::string name;

int power;

Creature(std::string a):name(a) {}

public:

std::string Name() const { return name;}

bool Alive() const { return power > 0;}

virtual void Transmute(int &gound) = 0;

virtual ~Creature(){}

};

Fontos, hogy az Transmute() metódus virtuális legyen, hiszen ez jelzi


a fordítónak, hogy egy ilyen metódus hívását nem szabad fordítási időben
kiértékelni, majd csak futás közben dől el, hogy a származtatott osztályok
közül melyiknek a Transmute() metódusa fut le (dinamikus kötés). Ez
pedig attól függ majd, hogy valójában milyen típusú objektumra mutat az a
hivatkozás, amellyel ezt metódust meghívjuk.

631
Speciális lények osztályai

A specifikáció meghatározta a Creature osztály leszármazott osztályait is.


Ezek rendelkeznek az ősosztály védett tagjaival, csak konstruktort kell
megadniuk és a Transmute() metódust felüldefiniálniuk.
A konstruktorok az adott fajtájú lényre jellemző kezdeti életerőt
állítják be azt követően, hogy az ősosztály konstruktorát meghívva beállítják
az objektum (konkrét lény) nevét is. Ez a név a konstruktor bemenő
paramétere.

class Greenfinch : public Creature {

public:

Greenfinch(std::string a):Creature(a){power=10;}

void Transmute(int &gound);

};

class Sandbug : public Creature {

public:

Sandbug(std::string a):Creature(a){power = 15;}

void Transmute(int &gound);

};

class Squelchy : public Creature {

public:

Squelchy(std::string a):Creature(a){power = 20;}

void Transmute(int &gound);

};

632
A Transmute() metódus deklarációját az utódosztályok megismétlik,
majd a specifikációban megadott táblázat alapján háromféleképpen
definiálják.

void Greenfinch::Transmute(int &gound)

switch(gound){

case 1: power+=1; break;

case 0: power-=2; break;

case 2: power-=1; gound = 1; break;

default:;

void Sandbug::Transmute(int &gound)

switch(gound){

case 1: power-=2; gound = 0; break;

case 0: power+=3; break;

case 2: power-=4; gound = 1; break;

default:;

633
void Squelchy::Transmute(int &gound)

switch(gound){

case 1: power-=2; gound = 2; break;

case 0: power-=5; break;

case 2: power+=6; break;

default:;

Ez mindhárom esetben egy elágazás, amely a bemenetként megadott


tereptől függően módosít a lény életerején és megváltoztatja, ha kell, a
terepet. Feltételezzük, hogy ezek a metódusok csak akkor kerülnek
meghívásra, amikor a lény még él, ezért ezt itt külön nem ellenőrizzük.

Tesztelés

Fekete doboz tesztesetek:


Érvényes adatok:
1. Nincsenek lények.
2. Nulla hosszúságú a pálya (minden lény életben marad).
3. Az első illetve az utolsó mezőre lépve fogy el egy lény életereje.
4. Egy speciális lény kipróbálása (mindhárom fajtára külön-külön) olyan
pályán, ahol egymás után mindhárom talaj előfordul, és ezeken a
lény végig megy (életben marad). Ehhez a teszthez érdemes kiíratni a
megváltozott pályát, hogy a változásokat számszerűen is láthassuk.
5. Egy speciális lény kipróbálása (mindhárom fajtára külön-külön) olyan
pályán ahol a lény életereje elfogy.
6. Általános eset sok lénnyel.

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

635
Teljes program

main.cpp:
#include <iostream>

#include <fstream>

#include <vector>

#include "creature.h"

using namespace std;

int main()

ifstream f("input.txt");

int n;

f >> n;

vector<Creature*> creatures(n);

for(int i=0; i<n; ++i){

char l;

string a;

f >> l >> a;

switch(l){

case 'T' : creatures[i] = new Squelchy(a);

break;

case 'Z' : creatures[i] = new Greenfinch(a);

636
break;

case 'B' : creatures[i] = new Sandbug(a);

break;

default:;

int m;

f >> m;

vector<int> palya(m);

for(int j=0; j<m; ++j) f >> palya[j];

for(int i=0; i<n; ++i){

for(int j=0; creatures[i]->Alive() && j<m;

++j){

creatures[i]->Transmute(palya[j]);

if (creatures[i]->Alive())

cout << creatures[i]->Name() << endl;

for(int i=0; i<n; ++i) delete creatures[i];

return 0;

637
creature.h:
#ifndef CREATURE_H

#define CREATURE_H

#include <string>

class Creature {

protected:

std::string name;

int power;

Creature(std::string a):name(a) {}

public:

std::string Name() const { return name;}

bool Alive() const { return power > 0;}

virtual void Transmute(int &gound) = 0;

virtual ~Creature(){}

};

class Greenfinch : public Creature {

public:

Greenfinch(std::string a):Creature(a){power=10;}

void Transmute(int &gound);

};

638
class Sandbug : public Creature {

public:

Sandbug(std::string a):Creature(a){power = 15;}

void Transmute(int &gound);

};

class Squelchy : public Creature {

public:

Squelchy(std::string a):Creature(a){power = 20;}

void Transmute(int &gound);

};

#endif

639
creature.cpp:
#include "creature.h"

using namespace std;

void Greenfinch::Transmute(int &gound)

switch(gound){

case 1: power+=1; break;

case 0: power-=2; break;

case 2: power-=1; gound = 1; break;

default:;

void Sandbug::Transmute(int &gound)

switch(gound){

case 1: power-=2; gound = 0; break;

case 0: power+=3; break;

case 2: power-=4; gound = 1; break;

default:;

640
void Squelchy::Transmute(int &gound)

switch(gound){

case 1: power-=2; gound = 2; break;

case 0: power-=5; break;

case 2: power+=6; break;

default:;

641
36. Feladat: Lengyel forma és kiértékelése

Alakítsunk át egy infix formájú, egész számokból, alapműveleti jelekből és


zárójelekből álló aritmetikai kifejezést postfix (lengyel) formájúra, és
számoljuk ki az értékét.

Specifikáció

A feladat specifikációja önmagában nem ad túl sokat árul el a


lehetséges megoldásról.
*
A = ( a : String , z : ℤ )
Ef = ( a = a’ )
Uf = ( z = érték(a’) )

A feladatot célszerű három részre felbontani:


1. Először a bemeneti adatként kapott karaktersorozatban ki kell jelölni a
szintaktikai egységeket (a zárójeleket, a műveleti vagy operátor jeleket
és az operandusokat). Ha például a bemenet a ”(11+26)*(43–4)”
sztring, akkor azt át kell alakítani egy token- sorozattá: <(> <11> <+>
<26> <)> <*> <(> <43> <–> <4> <)>, amelyben önálló elemek a
szintaktikai egységek.
* *
A = ( a : String , x : Token )
Ef = ( a = a’ )
Uf = ( x = tokenizált(a’) )
2. Az előző lépés eredményeként előállt infix formájú token-sorozatból
elkészíteni annak <11> <26> <+> <43> <4> <–> <*> postfix (lengyel)
formájú alakját.
* *
A = ( x : Token , y : Token )
Ef = ( x = x’ infixforma(x) )
Uf = ( y = InfixbőlPostfix(x’) )
3. A postfix formájú token-sorozatnak ki kell számolni az értékét.

642
*
A = ( y : Token , z : ℤ )
Ef = ( y = y’ postfixforma(y) )
Uf = ( z = kiértékel(y’) )
Ezeknek a részfeladatoknak jól látszik a bemenő és kimenő adatuk,
valamint az is, hogy egymás után megoldva őket az eredeti feladat
megoldásához jutunk.
Az egyes átalakítások során fel kell készülni arra, hogy ha az aritmetikai
kifejezést kezdetben nem adták meg helyesen, akkor a feldolgozás során
nem várt esetek fordulhatnak elő, amelyeket kezelni kell.

Absztrakt program

A megoldás központi eleme a szintaktikai egységeket, a tokeneket leíró


adattípus. Ez egy alternatív szerkezetű típus, hiszen legjellemzőbb
tulajdonsága az, hogy különböző fajtájú értékei lehetnek, amelyekről minden
pillanatban el kell tudnunk dönteni, hogy az egy nyitó vagy csukózárójel-e,
operandus-e vagy operátor. Az ilyen típus megvalósításához a származtatás
eszközét használjuk fel.

14-2. ábra. Tokenek osztálydiagrammja

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

x.First() y:=<>

x.End()
t: = x.Current()
t.Is_Operand() t.Is_LeftP() t.Is_RightP() t.Is_Operator()

s.Empty()
y:Hiext(t) s.Push(t) s.Top().Is_Left() s.Top().Is_Left()
s.Top().Priority()

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

645
értékeit, amelyek itt egész számok. Ha elkészítjük a 33. feladatban szereplő
Stack osztálynak olyan egy sablonját, amellyel olyan vermek írhatók le, ahol
az elemek típusát egy sablon-paraméter helyettesíti, akkor ezt
felhasználhatjuk mind tokeneket tároló verem, mind egész számokat tároló
verem létrehozásához.

Implementálás

A program komponens szerkezete

A program több részből áll. A stack.hpp állományban a verem


osztály-sablonját, a biqueue.hpp állományban helyezzük el a kettős sor
osztály-sablonját, a token.h és token.cpp állományok tartalmazzák a
tokenek ősosztályát és a tokenek fajtáinak az ősosztályból származtatott
osztályait. A main.cpp állomány main függvényben találjuk a feldolgozás
három lépését.

Tokenek osztályai

A Token osztály, valamint az abból származtatott osztályok definícióit


a terv alapján készítjük el.

class Token{

friend std::istream& operator>>(std::istream&,

Token*&);

public:

class IllegalElementException{

private:

char ch;

public:

IllegalElementException(char c) : ch(c){}

646
char Message() const { return ch;}

};

virtual ~Token(){}

virtual bool Is_LeftP() 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

647
fontos eleme a megoldásunknak, ugyanis ezzel az operátorral tudjuk egy
tetszőleges adatfolyam karaktersorozatából beolvasni a soron következő
tokennek megfelelő karaktereket és magát a tokent létrehozni.

istream& operator >> (istream& s, Token* &t) {

char ch;

s >> ch;

switch(ch){

case '0' : case '1' : case '2' : case '3' :

case '4' : case '5' : case '6' : case '7' :

case '8' : case '9' :

s.putback(ch);

int intval;

s >> intval;

t = new Operand(intval);

break;

case '+' : case '-' : case '*' : case '/':

t = new Operator(ch); break;

case '(' : t = new LeftP(); break;

case ')' : t = new RightP(); break;

case ';' : t = new End(); break;

default: if(!s.fail()) throw new

Token::IllegalElementException(ch);

return s;

648
}

A Token osztályban definiáljuk azt a kivétel-osztályt, amelynek


példányait kivételként dobjuk a tokenizálás során, ha nem megfelelő
karakterrel találkozunk.

Tárolók osztály-sablonjai

A tárolók (vermek és a kettős sorok) osztály-sablonjainak


elkészítésekor a korábban már létrehozott Stack és BiQueue osztályokból
indulunk ki (lásd előző fejezet feladatait).

Ahhoz, hogy a Stack osztályból sablont készítsünk, meg kell


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

649
szabad Item-re cserélni (ilyen például az enumeratorCount adattag
típusa).
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: "

650
<< ex->Message() << endl;

delete ex;

DeallocateToken(x);

exit(1);

A második szakasz a tervben megadott algoritmust kódolja. Az x kettős


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

651
token van (lásd a kifejezést beágyazó ciklus feltételét). Ha ilyen
bizonyosságunk nem lenne és csak futási időben derülhetne ki, hogy egy
ilyen átalakítás helyes-e vagy sem, akkor az úgynevezett dinamikus
konverziót kellene alkalmazni ((dynamic_cast<Operator*>(s.Top()))).
A megoldás harmadik szakaszának kódja a tervben megadott második
algoritmust követi, de ebben is elhelyezünk néhány hibaellenőrzést.
Végül megjegyezzük, hogy a kódban sehol sem figyelünk a vermek
esetleges FULLSTACK kivételére.

Tesztelés

A BiQueue és a Stack tesztelését már korábban megtettük.


A feladat fekete doboz tesztelésének keretében az érvényes
tesztadatok a szintaktikusan helyes aritmetikai kifejezések lesznek. Ezek
között feltételnül meg kell vizsgálni a többszörösen zárójelezett
kifejezéseket, olyanokat, ahol különböző priorítású műveleti jelek különféle
sorrendben fordulnak elő egy zárójelezetlen részben. Érvénytelen adatok a
különféle szintaktikusan helytelen kifejezések.
A tesztesetek részletes kidolgozását az Olvasóra bízzuk.

652
Teljes program

main.cpp:

#include <iostream>

#include <cstdlib>

#include „token.h”

#include „stack.hpp”

#include „biqueue.hpp”

using namespace std;

void DeallocateToken(BiQueue<Token*> &x);

int main()

cout << „Add meg az aritmetikai kifejezést!\n”;

cout << „Írj a végére pontosvesszőt!\n”;

// Tokenizálás

BiQueue<Token*> x;

try{

Token *t;

653
cin >> t;

while(!t->Is_End()){

x.Hiext(t);

cin >> t;

}catch(Token::IllegalElementException *ex){

cout << „Illegális karakter: „

<< ex->Message() << endl;

delete ex; DeallocateToken(x); exit(1);

654
// Lengyel formára hozás

BiQueue<Token*> y;

Stack<Token*> s;

BiQueue<Token*>::Enumerator itx

= x.CreateEnumerator();

for(itx.First(); !itx.End(); itx.Next()){

Token *t = itx.Current();

if(t->Is_Operand()) 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);

655
}

}else if (t->Is_Operator()) {

while(!s.Empty()

&& s.Top()->Is_Operator()

&& ((Operator*)s.Top())->Priority()

> ((Operator*)t)->Priority() )

y.Hiext(s.Pop());

s.Push(t);

}else{

cout << „Szintaktikai hiba!” << endl;

DeallocateToken(x);

exit(1);

while(!s.Empty()){

if(s.Top()->Is_LeftP()){

cout << „Szintaktikai hiba!” << endl;

DeallocateToken(x);

exit(1);

}else y.Hiext(s.Pop());

656
// Kiértékelés

try{

Stack<int> v;

BiQueue<Token*>::Enumerator ity

= y.CreateEnumerator();

for(ity.First(); !ity.End(); ity.Next()){

Token *t = ity.Current();

if (t->Is_Operand())

v.Push( ((Operand*)t)->Value() );

else

v.Push( ((Operator*)t)->

Evaluate(v.Pop(),v.Pop()) );

int r = v.Pop();

if(!v.Empty()) {

cout << „Szintaktikai hiba!” << endl;

DeallocateToken(x);

exit(1);

cout << „A kifejezes erteke: „ << r << endl;

}catch(Stack<int>::Exceptions ex){

if(Stack<int>::EMPTYSTACK == ex){

657
cout << „Szintaktikai hiba!” << endl;

DeallocateToken(x);

exit(1);

DeallocateToken(x);

return 0;

void DeallocateToken(BiQueue<Token*> &x)

BiQueue<Token*>::Enumerator itx

= x.Createenumerator();

for(itx.First(); !itx.End(); itx.Next()){

delete itx.Current();

658
token.h:

#ifndef TOKEN_H

#define TOKEN_H

#include <string>

#include <sstream>

class Token{

friend std::istream& operator>>(std::istream&,

Token*&);

public:

class IllegalElementException{

private:

char ch;

public:

IllegalElementException(char c) : ch®{}

char Message() const { return ch;}

};

virtual ~Token(){}

virtual bool Is_LeftP() const {return false;}

virtual bool Is_RightP() const {return false;}

virtual bool Is_Operand() const {return false;}

659
virtual bool Is_Operator() const {return false;}

virtual bool Is_End() const {return false;}

};

class Operand: public Token{

public:

Operand(int v) {val=v;}

bool Is_Operand() const {return true; }

int Value() const {return val;}

protected:

int val;

};

660
class Operator: public Token{

public:

Operator(char o) {op = o;};

bool Is_Operator() const {return true; }

int Priority() const;

int Evaluate(int a, int b) const;

protected:

char op;

};

class RightP: public Token{

public:

bool Is_RightP() const {return true; }

};

class LeftP: public Token{

public:

bool Is_LeftP() const {return true; }

};

class End: public Token{

public:

bool Is_End() const {return true; }

};

661
#endif

662
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

663
Token::IllegalElementException(ch);

return s;

int Operator::Priority() const

switch(op){

case ’+’ : case ’-’ : return 1;

case ’*’ : case ’/’ : return 2;

default: return 3;

int Operator::Evaluate(int a, int b) const

switch(op){

case ’+’: return a+b;

case ’-’: return a-b;

case ’*’: return a*b;

case ’/’: return a/b;

default:;

return 0;

stack.hpp:

664
#ifndef STACK_HPP

#define STACK_HPP

#include <iostream>

#include <memory>

template <typename Item>

class Stack{

public:

enum Exceptions{EMPTYSTACK, FULLSTACK};

Stack();

~Stack();

Stack(const Stack&);

Stack& operator=(const Stack&);

void Push(const Item &e);

Item Pop();

Item Top() const;

bool Empty() const;

private:

struct Node{

Item val;

Node *next;

665
Node(const Item &e, Node *n)

: val®, next(n){}

};

Node *head;

};

template <typename Item>

Stack<Item>::Stack(): head(NULL){}

template <typename Item>

Stack<Item>::~Stack()

Node *p;

while(head != NULL){

p = head;

head = head->next;

delete p;

template <typename Item>

void Stack<Item>::Push(const Item &e)

666
try{ head = new Node(e,head);}

catch(std::bad_alloc o){ throw FULLSTACK;}

template <typename Item>

Item Stack<Item>::Pop()

if(NULL == head) throw EMPTYSTACK;

Item e = head->val;

Node *p = head;

head = head->next;

delete p;

return e;

template <typename Item>

Item Stack<Item>::Top()const

if(NULL == head) throw EMPTYSTACK;

return head->val;

template <typename Item>

bool Stack<Item>::Empty()const

return NULL == head;

667
template <typename Item>

Stack<Item>::Stack(const Stack& s)

if(NULL == s.head) head = NULL;

else {

try{

head = new Node(s.head->val,NULL);

}catch(std::bad_alloc o){

throw FULLSTACK;

Node *q = head;

Node *p = s.head->next;

while(p != NULL){

try{ q->next = new Node(p->val,NULL);}

catch(std::bad_alloc o){throw FULLSTACK;}

q = q->next;

p = p->next;

template <typename Item>

Stack<Item>& Stack<Item>::operator=(const Stack& s)

668
if(&s == this) return *this;

Node *p;

while(head != NULL){

p = head;

head = head->next;

delete p;

if(NULL == s.head) head = NULL;

else {

try{ head = new Node(s.head->val,NULL);}

catch(std::bad_alloc o){throw FULLSTACK;}

Node *q = head;

Node *p = s.head->next;

while(p != NULL){

try{ q->next = new Node(p->val,NULL);}

catch(std::bad_alloc o){throw FULLSTACK;}

q = q->next;

p = p->next;

return *this;

669
}

#endif

670
biqueue.hpp:

#ifndef BIQUEUE_HPP

#define BIQUEUE_HPP

#include <memory>

template <typename Item>

class BiQueue{

public:

enum Exceptions{EMPTYSEQ, UNDERTRAVERSAL};

BiQueue():

first(NULL),last(NULL),enumeratorCount (0){}

BiQueue(const BiQueue&);

BiQueue& operator=(const BiQueue&);

~BiQueue();

void Loext(const Item &e);

Item Lopop();

void Hiext(const Item &e);

Item Hipop();

private:

struct Node{

Item val;

Node *next;

Node *prev;

Node(const Item &c, Node *n, Node *p)

671
: val®, next(n), prev(p){};

};

Node *first;

Node *last;

int enumeratorCount;

public:

class Enumerator{

public:

Enumerator(BiQueue *p):bq(p),current(NULL)

{++(bq->enumeratorCount);}

~Enumerator(){--(bq->enumeratorCount);}

Item Current()const {return current->val;}

void First() {current = bq->first;}

bool End() const {return NULL == current;}

void Next() {current = current->next;}

private:

BiQueue *bq;

Node *current;

};

Enumerator CreateEnumerator()

{return Enumerator(this);}

};

template <typename Item>

BiQueue<Item>::~BiQueue(){

672
Node *p, *q;

q = first;

while( q != NULL){

p = q;

q = q->next;

delete p;

template <typename Item>

BiQueue<Item>::BiQueue(const BiQueue &s){

if(NULL == s.first)first = last = NULL;

else{

Node *q = new Node(s.first->val,NULL,NULL);

first = q;

for(Node *p=s.first->next;p!=NULL;p=p->next){

q = new Node(p->val,NULL,q);

q->prev->next = q;

last = q;

template <typename Item>

673
BiQueue<Item>& BiQueue<Item>::operator=(

const BiQueue &s){

if(&s == this) return *this;

Node *p = first;

while(p != NULL){

Node *q = p->next;

delete p;

p = q;

if(NULL == s.first) first = last = NULL;

else{

Node *q = new Node(s.first->val,NULL,NULL);

first = q;

for(Node *p=s.first->next;p!=NULL;p=p->next){

q = new Node(p->val,NULL,q);

q->prev->next = q;

last = q;

return *this;

template <typename Item>

void BiQueue<Item>::Loext(const Item &e){

674
Node *p = new Node(e,first,NULL);

if(first != NULL) first->prev = p;

first = p;

if(NULL == last) last = p;

template <typename Item>

Item BiQueue<Item>::Lopop(){

if(enumeratorCount != 0) throw UNDERTRAVERSAL;

if(NULL == first) throw EMPTYSEQ;

int e = first->val;

Node *p = first;

first = first->next;

delete p;

if(first != NULL) first->prev = NULL;

else last = NULL;

return e;

template <typename Item>

void BiQueue<Item>::Hiext(const Item &e){

Node *p = new Node(e,NULL,last);

if(last != NULL) last->next = p;

last = p;

675
if(NULL == first) first = p;

template <typename Item>

Item BiQueue<Item>::Hipop(){

if(enumeratorCount != 0) throw UNDERTRAVERSAL;

if(NULL == last)throw EMPTYSEQ;

int e = last->val;

Node *p = last;

last = last->prev;

delete p;

if(last != NULL) last->next = NULL;

else first = NULL;

return e;

#endif

676
37. Feladat: Bináris fa bejárása

Készítsünk egy bináris fa-típust! A típusnak támogatnia kell a fa pre-, in- és


postorder bejárását! Egy bejárásnál paraméterként lehessen megadni azt a
tevékenységet, amit a bejáráskor az egyes csúcsokon kell majd végrehajtani!
Definiáljunk ilyen tevékenységeket a fa csúcsaiban tárolt értékek kiírására, a
csúcsbeli értékek összegzésére és a belső csúcsbeli értékek maximumának
meghatározására!

Specifikáció

A bináris fa típusának jellemző műveletei:


Preorder bejárás
Inorder bejárás
Postorder bejárás
Új érték új csúcsként való beillesztése
Eldönteni, hogy egy csúcs levélcsúcs-e
Eldönteni, hogy egy csúcs belső csúcs-e

A három bejárás mindegyikének paraméterként egy úgynevezett


tevékenység objektumot lehet majd átadni. A tevékenység objektum
rendelkezik egy „hajts vége” művelettel, amelyik bemenő adatként a
bejáráskor érintett csúcsot kapja meg. A bejárás végig adogatja a
tevékenység objektumot a bináris fa csúcsain, és minden csúcsra rendre
meghívja a tevékenység objektum „hajts vége” műveletét. Bizonyos
tevékenységek valamilyen eredményt számolnak a bejárt csúcsok értékeiből
(összeg, maximális érték stb.), ezért egy tevékenység objektum rendelkezhet
privát adattagokkal, amelyeket az objektum létrehozása inicializál, a „hajts
vége” művelete módosít, és ezek természetesen lekérdezhetőek.
A „fába új elemet beszúr” műveletet ismételt meghívásával leszünk
majd képesek egy bináris fát felépíteni. Ezt a műveletet most úgy valósítjuk

677
majd meg, hogy az a fának egy véletlenszerűen kiválasztott ágának végére
függessze fel az új csúcsot.

Absztrakt program

A feladat megoldásához több egymáshoz szorosan kapcsolódó osztá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.

678
Item Item

Item Item

14-3. ábra. Bináris fa osztálydiagrammja

A bejárások egy tevékenység objektumot kapnak paraméterként. Ez


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

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

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

Item

Item Item

14-4. ábra. Tevékenységek osztálydiagrammja

Implementálás

A program komponens szerkezete

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

682
Action osztály

template < typename Item>

class Action{

public:

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;

};

683
LinkedNoded osztály

A Node osztály-sablonból származtatjuk a LinkedNode osztály-sablont.


Kiegészítjük a bal illetve jobboldali gyerekére mutató pointertagokkal,
felüldefiniáljuk az IsLeaf() metódust, hiszen most már a gyerekekre
mutató pointerek értéke alapján ez a tulajdonság kiszámolható, és ezzel
implicit módon az IsInternal()-t is definiáljuk.
Mivel meg akarjuk engedni, hogy a BinTree osztály-sablon lássa a
LinkedNode osztály-sablon privát tagjait, ezért a BinTree osztály-sablont
barátként kell megjelölni. Tekintettel azonban arra, hogy a fordító itt még
nem tudhatja mi az a BinTree, ezért a LinkedNode osztály-sablon előtt
deklarálni kell azt.

template < typename Item> class BinTree;

template < typename Item>

class LinkedNode: public Node<Item>{

friend class BinTree;

public:

LinkedNode(const Item& v,

LinkedNode *l, LinkedNode *r)

:Node<Item>(v), left(l), right®{}

bool IsLeaf() const

{return NULL == left && NULL == right;}

private:

LinkedNode *left;

LinkedNode *right;

};

684
Bintree osztály

A bináris fa osztály-sablonjának publikus része elsőként egy üres fát


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

template < typename Item>

class BinTree{

public:

BinTree():root(NULL){srand(time(NULL));}

virtual ~BinTree();

void RandomInsert(const Item& e);

void PreOrder (Action<Item> *todo)

685
{Pre (root, todo);}

void InOrder (Action<Item> *todo)

{In (root, todo);}

void PostOrder(Action<Item> *todo)

{Post(root, todo);}

protected:

LinkedNode<Item> *root;

void Pre(LinkedNode<Item> *r,Action<Item>*todo);

void In(LinkedNode<Item> *r, Action<Item>*todo);

void Post(LinkedNode<Item> *r,Action<Item>*todo);

BinTree(const BinTree&);

BinTree& operator=(const BinTree&);

};

A bináris fa bejáró műveletei a bejárásokat rekurzív módon írják le.


Ezen belül a todo által mutatott tevékenység Exec() metódusát kell az
aktuális csúcsra meghívni. Itt is tanúi lehetünk a dinamikus kötés
jelenségének. A todo->Exec() hívást ugyanis nem lehet fordítási időben
meghatározni (erre figyelmeztet az Exec() metódus virtuális volta), hiszen
nem tudhatjuk, hogy konkrétan milyen típusú tevékenység objektumra
mutat a todo.
Az alábbi metódusokban jól látható a három féle fabejárást végző
rekurzív algoritmus. Üres részfa esetén egyik sem kezdeményez rekurzív
hívást, nem üres részfa esetén a megfelelő sorrendben történik a részfa
gyökerének todo->Exec() általi feldolgozása és a bal illetve jobboldali

686
részfára történő rekurzív hívás. Mivel egyre kisebb részfákra hívjuk meg az
alprogramot, véges lépésen belül üres részfákhoz fogunk jutni, azaz nem
fordulhat elő a rekurzív hívásoknak végtelen hosszú láncolata.

template < typename Item>

void BinTree<Item>::

Pre(LinkedNode<Item> *r,Action<Item> *todo)

if(NULL == r) return;

todo->Exec(r);

Pre(r->left, todo);

Pre(r->right, todo);

template < typename Item>

void BinTree<Item>::

In(LinkedNode<Item> *r, Action<Item> *todo)

if(NULL == r) return;

In(r->left, todo);

todo->Exec(r);

In(r->right, todo);

template < typename Item>

687
void BinTree<Item>::

Post(LinkedNode<Item> *r,Action<Item> *todo)

if(NULL == r) return;

Post(r->left, todo);

Post(r->right, todo);

todo->Exec(r);

Érdekes és egyben hasznos alkalmazása a tevékenység objektumoknak


a bináris fa egy csúcsát megszüntető tevékenység létrehozása. Ennek típusát
az alábbi osztály írja le.

template < typename Item>

class DelAction: public Action<Item>{

public:

void Exec(Node<Item> *node){delete node;}

};

Ha példányosítunk egy ilyen tevékenység objektumot és végig vezetjük


őt a bináris fán a postorder bejárással, akkor ezzel felszabadítjuk a fa összes
csúcsát, azaz megszüntetjük a fát. A bináris fa destruktorának éppen erre van
szüksége. (Vigyázat! A másik két bejárás erre nem alkalmas.)
Ha a DelAction osztály definícióját a bináris fa osztály-sablonjának
rejtett részébe ágyazzuk, akkor egyrészt nem kell előtte feltüntetni a
template <class Item> sort, másrészt elég a destruktorban
DelAction-t írni a DelAction<Item> del helyett.

688
template < typename Item>

BinTree<Item>::~BinTree()

DelAction del;

ost(root, &del);

Végül megadjuk a bináris fába új csúcsot véletlenszerűen beillesztő


metódus implementációját. Ez a metódus egy ciklusban generál
véletlenszerű „jobb illetve bal értékeket” amíg nem talál olyan csúcsot,
amelyiknek a legutoljára generált oldalán nincs csúcs. Ide függeszt fel a
beszúrandó értéket tartalmazó új csúcsot az alábbi kód.

template < typename Item>

void BinTree<Item>::RandomInsert(const Item& e)

if(NULL == root) root =

new LinkedNode<Item>(e,NULL,NULL);

else {

LinkedNode<Item> *r = root;

int d = rand();

while(d&1 ? r->left!=NULL : r->right!=NULL){

if(d&1) r = r->left;

else r = r->right;

d = rand();

689
}

if(d&1) r->left =

new LinkedNode<Item>(e,NULL,NULL);

else r->right =

new LinkedNode<Item>(e,NULL,NULL);

A kódban néhány olyan érdekes C++ nyelvi elem került, mint a


feltételes kifejezés (feltétel ? kifejezés1 : kifejezés2), vagy a
bitenkénti „és” művelet, amellyel leválasztjuk a véletlen szám legutolsó
bitjét, hogy azt jobb illetve bal értéknek tekintsük.

Tevékenység osztályok

Egy tevékenység osztályt már definiáltunk, ez volt a DelAction. Adjuk meg


most a többit is.
A Maxsearch típusú tevékenység megkeresi a belső csúcsokban a
legnagyobb értéket. A konstruktorban a keresés sikerességét jelző logikai
értéket inicializáljuk, ennek, valamint a max tagnak az értékét módosítja az
Exec(), az eredményt pedig a Found() és a MaxValue() segítségével
kérdezhetjük le.

class Maxsearch: public Action<int>{

public:

Maxsearch(){l = false;}

void Exec(Node<int> *node){

if(node->IsLeaf()){

if(!l){

690
l = true;

max = node->Value();

}else if(node->Value()>max)

max = node->Value();

bool Found(){return l;}

int MaxValue(){return max;}

private:

int max;

bool l;

};

A Summation típusú tevékenység hozzáadja az aktuális csúcs értékét


konstruktorban nullának inicializált s adattaghoz. A Result() ennek
aktuális értéket adja vissza.

691
class Summation: public Action<int>{

public:

Summation(): s(0){}

void Exec(Node<int> *node){s+=node->Value();}

int Result(){return s;}

private:

int s;

};

A Printer osztály-sablon tevékenység objektumai egy csúcs értékét


írják ki. A tevékenység paramétere a konstruktorában beállítható kimeneti
folyam. Az osztály-sablonból egy konkrét kiíró tevékenység a Printer<int>
print(cout) utasítással hozható belőle létre.

template < typename Item>

class Printer: public Action<Item>{

public:

Printer(ostream &o): s(o){};

void Exec(Node<Item> *node)

{s << ’[’<< node->Value() << ’]’;}

private:

ostream& s;

};

Főprogram

692
Az alábbi kód a szabványos bemenetről beolvasott értékekkel
véletlenszerűen épít fel egy bináris fát.

BinTree<int> t;

int i;

while(cin >> i) {

t.RandomInsert(i);

Ennek tartalmát különféle bejárási stratégiák mellett írjuk ki a standard


kimenetre, majd meghatározzuk a csúcsokban tárolt értékek összegét és a
belső csúcsok értékeinek maximumát.

Printer<int> print(cout);

cout << „Preorder bejárás:”;

t.PreOrder(&print);

cout << endl;

cout << „Inorder bejárás:”;

t.InOrder(&print);

cout << endl;

cout << „Postorder bejárás:”;

t.PostOrder(&print);

cout << endl;

693
Summation sum;

t.PreOrder(&sum);

cout << „Fa elemeinek összege:”

<< sum.Result() << endl;

Maxsearch ms;

t.PreOrder(&ms);

cout << „Maximum of internal elements:\n”;

if(ms.Found()) cout << ms.MaxValue() << endl;

else cout << „none” << endl;

694
Tesztelés

Egy olyan programot nehéz tesztelni, amelyik véletlenszerűen állítja elő a


bemenő adatokat. Szerencsére most nem egészen erről van szó, hiszen a
bináris fába betett értékeket mi adhatjuk meg, csak azok fába beillesztése
véletlenszerű. Ez nem akadályozza meg a fa kiírásának tesztelését, az
összegzés tesztelését, egyedül a maximumkeresés esetében nehéz az olyan
tesztadatok előállítása, amikor a legelső, vagy a legutolsó adatot szeretnék
maximálisnak választani. Ha másképpen nem megy, külön tesztprogramot
kell készíteni. Külön komponens tesztet nem készítünk, az alábbi tesztesetek
kielégítőek.

Fekete doboz tesztesetek:


Érvényes adatok:
1. Üres fa esete.
2. Egyetlen csúcs (gyökércsúcs, amely ilyenkor levél csúcs is) esete.
3. Két csúcs beillesztése. (Egy belső csúcs és egy levélcsúcs lesz)
4. Több csúcs, legalább két belső csúcs beillesztése. Ehhez
próbálgatással juthatunk el. (Itt érdemes az összegzést illetve a
feltételes maximumkeresést nemcsak a preorder, hanem a másik
kettő stratégiával is kipróbálni.)
5. Általános eset.
Érvénytelen adatok nem lehetnek, viszont a memória elfogyást ki lehet
mérni, de erre nem alkalmas a jelenlegi főprogram, hiszen az interaktív.

Fehér doboz tesztesetek:


1. Erre még inkább igaz az, amit a véletlen feltöltésről az előbb
mondtunk. Nehéz például letesztelni a RandomInsert() metódus
minden utasítását. Nagyszámú adat megadása esetén azonban
legalább 50%-os valószínűséggel minden utasítására rákerül a
vezérlés. Ugyanez mondható el a bejárások utasításairól is.
2. Dinamikus helyfoglalások miatt vizsgálni kellene még a
memóriaszivárgást.

695
Teljes program

main.cpp:

#include "bintree.hpp"

#include <iostream>

using namespace std;

template < typename Item>

class Printer: public Action<Item>{

ostream& s;

public:

Printer(ostream &o): s(o){}

void Exec(Node<Item> *node)

{s << '['<< node->Value() << ']';}

};

class Summation: public Action<int>{

public:

Summation(): s(0){}

void Exec(Node<int> *node){s+=node->Value();}

int Result(){return s;}

private:

int s;

696
};

class Maxsearch: public Action<int>{

public:

Maxsearch(){l = false;}

void Exec(Node<int> *node){

if(node->IsLeaf()){

if(!l){

l = true;

max = node->Value();

}else if(node->Value()>max)

max = node->Value();

bool Found(){return l;}

int MaxValue(){return max;}

private:

int max;

bool l;

};

697
int main()

BinTree<int> t;

int i;

while(cin >> i) {

t.RandomInsert(i);

Printer<int> print(cout);

cout << "Preorder bejárás:";

t.PreOrder(&print);

cout << endl;

cout << "Inorder bejárás:";

t.InOrder(&print);

cout << endl;

cout << "Postorder bejárás:";

t.PostOrder(&print);

cout << endl;

Summation sum;

t.PreOrder(&sum);

698
cout << "Fa elemeinek összege:"

<< sum.Result() << endl;

Maxsearch ms;

t.PreOrder(&ms);

cout << "Maximum of internal elements:\n";

if(ms.Found()) cout << ms.MaxValue() << endl;

else cout << "none" << endl;

return 0;

699
bintree.hpp:

#ifndef BINTREE_H

#define BINTREE_H

#include <cstdlib>

#include <time.h>

template < typename Item>

class Node {

public:

Item Value() const {return val;}

virtual bool IsLeaf() const = 0;

bool IsInternal() const {return !IsLeaf();}

protected:

Node(const Item &v): val(v){}

Item val;

};

template < typename Item>

class Action{

public:

virtual void Exec(Node<Item> *node)=0;

700
};

template < typename Item>

class BinTree{

public:

BinTree():root(NULL){srand(time(NULL));}

virtual ~BinTree();

void RandomInsert(const Item& e);

void PreOrder (Action<Item> *todo)

{Pre (root, todo);}

void InOrder (Action<Item> *todo)

{In (root, todo);}

void PostOrder(Action<Item> *todo)

{Post(root, todo);}

protected:

LinkedNode<Item> *root;

void Pre(LinkedNode<Item> *r,Action<Item>*todo);

void In(LinkedNode<Item> *r, Action<Item>*todo);

void Post(LinkedNode<Item> *r,Action<Item>*todo);

BinTree(const BinTree&);

BinTree& operator=(const BinTree&);

};

701
template < typename Item>

BinTree<Item>::~BinTree()

DelAction del;

Post(root, &del);

template < typename Item>

void BinTree<Item>::RandomInsert(const Item& e)

if(NULL == root) root =

new LinkedNode<Item>(e,NULL,NULL);

else {

LinkedNode<Item> *r = root;

int d = rand();

while(d&1 ? r->left!=NULL : r->right!=NULL){

if(d&1) r = r->left;

else r = r->right;

d = rand();

if(d&1) r->left =

new LinkedNode<Item>(e,NULL,NULL);

else r->right =

702
new LinkedNode<Item>(e,NULL,NULL);

template < typename Item>

void BinTree<Item>::

Pre(LinkedNode *r,Action<Item> *todo)

if(NULL == r) return;

todo->Exec(r);

Pre(r->left, todo);

Pre(r->right, todo);

template < typename Item>

void BinTree<Item>::

In(LinkedNode *r,Action<Item> *todo)

if(NULL == r) return;

In(r->left, todo);

todo->Exec(r);

In(r->right, todo);

703
template < typename Item>

void BinTree<Item>::

Post(LinkedNode *r,Action<Item> *todo)

if(NULL == r) return;

Post(r->left, todo);

Post(r->right, todo);

todo->Exec(r);

#endif

704
C++ kislexikon

absztrakt osztály class O{

// vagy privát konstruktor

private: Osztaly();

// vagy absztrakt metódus

void Method() = 0;

};

védett tag protected

publikus class O : public Os { };

származtatás
privát class O : private Os { };

származtatás
virtuális virtual void Method();

metódus
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;

705
Fv<int>(…)

706
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());

707
A bemutatott osztály-sablon könyvtár nem az ipari alkalmazások
számára készült. Nem hisszük, hogy a gyakorlatban való felhasználása
egyszerű illetve célszerű lenne. Egy programozási tétel (lényegében egy
ciklus) implementálása ugyanis önmagában nem túl nehéz feladat, ezért
sokkal könnyebb közvetlenül kódolni, mint egy összetett osztály-sablon
könyvtárból származtatni, hiszen ehhez a könyvtár elemeit kell pontosan
megismerni és helyesen alkalmazni. Ennél fogva ez a tanulmány rávilágít
arra a határra is, hogy mikor érdemes az objektum-orientált illetve generikus
nyelvi eszközöket bevetni egy feladat csoport megoldásánál és ez mikor nem
jelent már előnyt. Ugyanakkor ez a könyvtár nagyon is alkalmas a különféle
objektum-orientált implementációs technikák megmutatására. A könyvtár
használatával igen szép megoldásokat tudunk előállítani. Programozói
szemmel például nagyon érdekes, hogy az előállított megoldásokban
mindössze egyetlen ciklus lesz, mégpedig a programozási tételek ősosztá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

708
Minden olyan objektum, amelynek osztálya ebből származik,
rendelkezni fog a bejárás itt bevezetett négy alapműveletével. Ezek a
műveletek ezen a szinten nincsenek definiálva (absztraktak), a bejárt elemek
típusát pedig az Item sablonparaméter jelzi.
E fejezetben megoldott feladatok bemenő adatait egy szöveges
állomány tartalmazza, amelyet szekvenciális inputfájlként kívánunk
feldolgozni. Ezért célszerű kiegészíteni az osztály-sablon könyvtárat egy olyan
felsoroló osztállyal, amelynek objektumai szöveges állományra épített
szekvenciális inputfájl elemeit képesek sorban egymás után végigolvasni
(bejárni).

15-2. ábra. Szöveges állomány felsorolójának osztály-sablonja

A First() és a Next() a soron következő elemet olvassák be a df


változóba, ennek értékét kérdezi le a Current(), és ha a legutolsó olvasás
sikertelen volt, akkor az End() igazat fog visszaadni.
Habár olyan feladatot nem fogunk most látni, ahol egy tömb elemeit
kell feldolgozni, ez azért gyakori eset, ezért érdemes a könyvtárba felvenni az
egy-dimenziós tömb elemeit felsoroló osztályt.

709
15-3. ábra. Tömb felsorolójának osztály-sablon

Ennek reprezentációja a tömb mellett egy indexváltozót is tartalmaz,


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

710
15-4. ábra. A programozási tételek ősosztály-sablonja

A Run() metódus végleges változata néhány részletében eltér a


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

for (First(); LoopCond(); enor.Next())

Do(enor.Current());

A Procedure osztályból származtatjuk a programozási tételek


osztályait.

711
Az összegzés tételével többféle feladat-típust meg lehet oldani. A
szigorúan vett összegzés mellett ilyen például az összeszorzás, összefűzés,
feltételes összegzés, számlálás, másolás, kiválogatás, szétválogatás és
összefuttatás. Ezt az általánosságot tükrözi a Summation osztály-sablon.

Az Item sablonparaméter a feldolgozandó elemek típusára, a


ResultType paraméter az összegzés eredményének típusára utal. A
ResultType típusú result adattag az eredmény tárolására szolgál,
amelyet majd a Result() metódussal kérdezhetünk le.

15-5. ábra. Az összegzés osztály-sablonja

Az osztály egy feltételes tevékenység formájában implementálja a


Do() metódust ( if (Cond(e)) Add(e) ), ahol az e az éppen felsorolt
elem, amelyet a Do() metódus paramétereként megkap. A Cond() (itt
csak az alapértelmezés szerinti megvalósítását adhatjuk meg, amely mindig
igazat ad) ezen elem alapján ad vissza egy logikai értéket, amely ha igaz,
akkor az Add() metódus – ezen a szinten ez is absztrakt – ugyancsak ezen
elem alapján módosíthatja az eredményt. Az eredményt a konkrét
felhasználáskor az ősosztály absztrakt Init() metódusának
felüldefiniálásával kell majd inicializálni.
Talán jobban megértjük az összegzés osztály-sablonját, ha
származtatjuk belőle a számlálás programozási tételét leíró osztályt. Ez egy
speciális összegzés, amennyiben a ResultType típus ilyenkor az egész
számok típusa lesz, a result adattagot az Init() metódusban nulla
kezdőértékre kell beállítani, és ezt az értéket az Add() metódusban kell

712
eggyel növelni. Az általános számlálásnál a Cond() nem kap új jelentést,
viszont egy konkrét számlálásnál ezt kell majd felüldefiniálni.

15-6. ábra. A számlálás osztály-sablonja

A MaxSearch osztály-sablon az általános maximumkeresést


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

713
15-7. ábra. Az általános maximumkeresés osztály-sablonja

A Do() a feltételes maximumkeresés programozási tételéből ismert


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

714
15-8. ábra. A kiválasztás osztály-sablonja

Sajátos módon az Init()és a Do() metódus az üres utasítás lesz. A


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

715
15-9. ábra. A lineáris keresés osztály-sablonja

Azt, hogy a keresés pesszimista vagy optimista legyen, egy külön


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

716
Item Item

Item Item

Item,ResultType Item,Value,Compare Item Item,optimist

Item Item

Item

15-10. ábra. Programozási tételek osztály-sablon kód-könyvtára

Osztály-sablon könyvtár implementálása

A fent bevezetett osztály-sablonokat C++ nyelven készítjük el.


Az absztrakt felsoroló ősosztály sablonja a bejáró műveletek absztrakt
virtuális (tehát kötelezően felüldefiniálandó) metódusait írja le.

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

718
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

719
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(){}

720
};

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

721
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

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

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

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

725
}

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

726
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;}

727
Item Elem() const { return elem;}

};

728
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 )
e f'
e páratlan

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

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

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

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

732
}

733
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 = min e.létszám
e t'
e.idő hétvége
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.

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

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

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

737
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()

738
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

739
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 = search e.db 5 l név=p.név )
e t'
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ó) f.First() vége:= f.End() vége akt

akt : Pár Next() ha vége akkor

vége : akt.név:=f.Current()
összegez(akt, f)

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

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

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

743
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);

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

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

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

747
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

748
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();

749
}

750
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ó = min e.str )
e t'
e.vanW
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

751
akt : Szó szó_eleje(f) ha vége akkor

vége : Next() 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 := select e elválasztójel
e f'

752
e {W } elválasztójel
W_keres(f,akt) ~ akt.str,f := e
e f'

akt.vanW := e =’W’

e elválasztójel
szó_vége(f,akt) ~ akt.str,f := e
e f'

753
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);

754
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);

755
}

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

756
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

757
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() ||

758
(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 {

759
return e != ' ' && e != '\t'

&& e != '\r' && e != '\n';

void First(){}

};

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

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

762
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

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

764
&& 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(){

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

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

767
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(ℤ) x.First() lásd alább vége akt

y : enor(ℤ) y.First()

akt : ℤ Next()

vége :

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

768
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.End() ( y.End() x.End() y.End()


x.Current()< x.Current()> x.Current()=
y.Current()) y.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;}

769
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){

770
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()

771
&& 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();

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

773
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ú.

774
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

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

776
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);

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

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

779
#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;

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

781

You might also like