Professional Documents
Culture Documents
ProgramozasMegvalositas PDF
ProgramozasMegvalositas PDF
PROGRAMOZÁS
2. kötet
MEGVALÓSÍTÁS
1
Egyetemi jegyzet
2012
2
ELŐSZÓ .................................................................................................................... 8
BEVEZETÉS ............................................................................................................. 10
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
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
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
6
42. Feladat: Összefuttatás ........................................................... 767
IRODALOM JEGYZÉK ............................................................................................ 781
7
ELŐSZÓ
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
Feladat
Elemzés
Specifikáció
Tervezés
Programterv
Megvalósítás
Programkód
Tesztelés, Dokumentálás
Megoldás
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
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
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
Implementációs stratégia
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.
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
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.
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
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
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
Specifikáció
Absztrakt program
z := x mod y
Implementálás
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
#include <iostream>
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
int x, y, z;
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;
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.
cin >> x;
utasítás1;
32
utasítás2;
...
cin >> x;
if (x<0){
return 1;
cin >> y;
if (y<=0){
return 1;
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;
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
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";
cout << endl << x << " mod " << y << " = " << z;
Tesztelés
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:
Érvénytelen tesztesetek:
36
Teljes program
#include <iostream>
int main()
int x, y, z;
cin >> x;
if (x<0){
return 1;
37
}
cin >> y;
if (y<=0){
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
Specifikáció
A = ( x, y : ℤ )
Ef = ( x=x’ y=y’ )
Uf = ( x=y’ y=x’ )
Absztrakt program
x, y := y, x
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>
int main()
...
return 0;
Deklarációk
int x, y;
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
int z;
z = x;
x = y;
y = z;
vagy
x = x - y;
41
y = x + y;
x = y - x;
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.
cin >> x;
cin >> y;
Eredmény kiírása
Tesztelés
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
#include <iostream>
int main()
int x, y;
cin >> x;
cin >> y;
// Számítás
x = x - y;
y = x + y;
x = y - x;
// Eredmény kiírása
43
<< y << endl;
return 0;
C++ kislexikon
int main()
return 0;
44
2. Strukturált programok
Implementációs stratégia
c d ciklus- szekvencia
45
d := d–c c := c–d értékadás utasítás
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
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)
50
1.23 .23 0.23 1 1.0 1.2e10 1.23e-15 1e10
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
52
str.c_str() kifejezés az str-ben található karakterlánc C stílusú
megfelelője lesz.
Karakterlánc ábrázolása
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.
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.
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
{ ág_1 ág_1
ág_1 } }else{
} else{ ág_2
else ág_2 }
{ }
ág_2
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;
feltétel1
feltétel2
…
feltételn
ág1 ág2 … ágn SKIP
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
feltétel
mag
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
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.
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
Specifikáció
61
Absztrakt program
a 0
b x1,x2:= c
x1,x2:= x1:=
2a b d b
2a
Implementálás
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>
int main()
...
return 0;
Deklarációk
string valasz;
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) {
if (d<0){
else if (0 == d){
egy = true;
x1 = x2 = -b/(2*a);
ketto = true;
x1 = (-b+sqrt(d))/(2*a);
x2 = (-b-sqrt(d))/(2*a);
} else if (0 == a){
if (b != 0){
egy = true;
x1 = -c/b;
66
}
Eredmény megjelenítése
67
(ilyenkor semmit nem kell írni, ezért a harmadik ág üres, amely meg sem
jelenik a kódban).
if(egy){
}else if(ketto) {
Tesztelés
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
Teljes program
#include <iostream>
#include <string>
#include <cmath>
int main()
double a, b, c;
69
string valasz;
// Beolvasás
// Számolás
if (a != 0)
if (d<0){
else if (0 == d){
egy = true;
x1 = x2 = -b/(2*a);
70
else if (d > 0){
ketto = true;
x1 = (-b+sqrt(d))/(2*a);
x2 = (-b-sqrt(d))/(2*a);
else if (0 == a){
if (b != 0){
egy = true;
x1 = -c/b;
// Kiírás
71
cout << "x = " << x1;
return 0;
72
4. Feladat: Legnagyobb közös osztó
Specifikáció
Absztrakt program
d,c := n,m
c d
c<d d<c
d := d–c c := c–d
73
Implementálás
Program kerete
#include <iostream>
int main()
...
return 0;
Deklarációk
int m, n, d, c;
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;
75
cout << "Kérem a második számot: "; cin >> n;
return 1;
Eredmény kiírása
76
Tesztelés
Érvényes tesztesetek:
Érvénytelen 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
#include <iostream>
int main()
int m, n, d, c;
return 1;
78
//Főprogram: d:=lnko(m,n)
d = n; c = m;
while(c != d){
if (c<d) { d = d-c; }
return 0;
79
5. Feladat: Legnagyobb közös osztó még egyszer
Specifikáció
Absztrakt program
m 0
n, m := m, n mod m
80
Implementálás
Program kerete
#include <iostream>
int main()
...
return 0;
Deklarációk
while(m != 0){
int s = n;
n = m;
81
m = s mod m;
82
cout << "Természetes számokkal dolgozok!";
exit(1);
Eredmény kiírása
Tesztelés
Teljes program
#include <iostream>
#include <cstdlib>
83
int main()
int m, n;
exit(1);
//Főprogram: n:=lnko(m,n)
while(m != 0){
int s = n;
n = m;
m = s % m;
84
char ch; cin >> ch;
return 0;
85
C++ kislexikon
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
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
Implementációs stratégia
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.
Nyelvi elemek
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);
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.
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;
vector<vector<int>> t(n);
i:=1
i n vagy i:=1..n
94
eleje
feltétel
ciklusmag
továbblépés
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.
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 program kerete
#include <iostream>
int main()
...
// Maximum kiválasztás
...
// Eredmény kiírása
...
return 0;
97
}
if(v[i]>max){
Eredmény kiírása
98
Tömb feltöltése
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){
return 1;
for (int i=0; i<n; ++i) cout << v[i] << " ";
if (0 == n){
100
cout << "Nincs a tömbnek eleme!\n";
return 1;
for (int i=0; i<n; ++i) cout << v[i] << " ";
int v[maxsize];
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.
int v[maxsize];
int n;
cin >> n;
return 1;
102
cout << "Adja meg a tomb elemeit!\n";
cin >> n;
int v[n];
Részletesebben
int n;
103
cin >> n;
if(!(n>0)){
return 1;
int v[n];
4. Implementálás vector<>-ral
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);
vector<int> v;
int n;
cin >> n;
if(!(n>0)){
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();
int n = (int)v.size();
Tesztelés
Érvényes tesztesetek:
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:
107
108
Teljes program
#include <iostream>
#include <vector>
int main()
// Vektor definiálása
vector<int> v;
int n;
cin >> n;
if(!(n>0)){
return 1;
v.resize(n);
109
for (int i=0; i<(int)v.size(); ++i){
// Maximum kiválasztás
if (v[i]>max){
// Eredmény kiírása
cout << " ,amely a " << (ind+1) << ". elem.\n";
return 0;
110
7. Feladat: Mátrix maximális eleme
Specifikáció
Absztrakt program
Implementálás
111
#include <iostream>
#include <vector>
int main()
...
// Maximum kiválasztás
...
// Eredmény kiírása
...
return 0;
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;
t[i].resize(m);
cout << "t[" << i+1 << "," << j+1 << "]= ";
Maximum kiválasztás
113
for(int i = 0; i<(int)t.size(); ++i)
if(t[i][j]>max){
Eredmény kiírása
<< " ,amely a " << ind+1 << ". sor "
Tesztelés
Teljes program
#include <iostream>
#include <vector>
114
int main()
// Mátrix definiálása
int n, m;
cin >> n;
cin >> m;
return 1;
cout << "t[" << i+1 << "," << j+1 << "]= ";
// Maximum kiválasztás
115
for(int i = 0; i<(int)t.size(); ++i)
if(t[i][j]>max){
// Eredmény kiírása
<< " ,amely a " << ind+1 << ". sor "
return 0;
116
8. Feladat: Melyik szóra gondoltam
Specifikáció
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]) )
Absztrakt program
darab:=darab+1
ki
tipp := rejt
i = 1 .. rejt SKIP
tipp[i] rejt[i]
tipp[i] := ”.” SKIP
Implementálás
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.
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 += '.';
// Játékos szándéka
if(!ki){
// |tipp| := |rejt|
120
for(int i=(int)tipp.size();
// tipp kiértékelése
121
meg. A do-while utasítás alkalmazásával a ciklusmagot csak egyszer kell
kódolni.
ciklusmag
feltétel
ciklusmag
int darab = 0;
do{
// Belső szint
}while(!ki);
if(tipp == rejt)
122
123
Tesztelés
124
Teljes program
#include <iostream>
#include <string>
int main()
// Rejtvény beolvasása
string rejt;
int darab = 0;
string tipp;
bool ki;
do{
// Játékos szándéka
125
if(!ki){
// |tipp| := |rejt|
for(int i=(int)tipp.size();
// tipp kiértékelése
}while(!ki);
// Eredmény kiírása
if(tipp == rejt)
return 0;
126
C++ kislexikon
sizeof(v)/sizeof(v[0]);
int size;
int size;
vector<Element> v(size);
127
… // sizei értéke: 0≤sizei≤maxi
(téglalap …
mátrix)
int sizei, sizej;
vector<vector<Element>> a(sizei)
for(int i=0,i<sizei,++i)
t.resize(sizej);
128
4. Konzolos be- és kimenet
Implementációs stratégia
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.
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.
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
>>
cin változó
cout <<
kifejezés
132
4-3. ábra. Szabványos input/output
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
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)
char ch;
do{
...
136
}while(ch != 'n' && ch != 'N');
return 0;
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.
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ó.
if(cin.fail()){
cin.clear();
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());
140
Különféle karakterláncok
141
9. Feladat: Duna vízállása
Specifikáció
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
max := v[1]
i = 2 .. n
v[i]>max
max := v[i] SKIP
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
int n;
cin >> n;
if(cin.fail() || n<=0){
return 1;
144
vector<int> v(n);
if(cin.fail() || v[i]<0){
return 1;
Kiírás
if (v[i]>max){
int m = 30;
145
cout << endl;
Tesztelés
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>
int main()
// Tömb definiálása
if(cin.fail() || n<=0){
return 1;
vector<int> v(n);
if(cin.fail() || v[i]<0){
148
return 1;
// Maximumkeresés
if (v[i]>max){
// Hisztogram rajzolása
int m = 30;
149
10. Feladat: Alsóháromszög-mátrix
Specifikáció
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
int n;
cin >> n;
vector<double> a(n*(n+1)/2);
cout << "a[" << i << "," << j << "]= ";
151
cin >> a[(i-1)*i/2 + j - 1];
Számolás
c[(i-1)*i/2+j-1]+=
a[(i-1)*i/2+k-1]*b[(k-1)*k/2+j-1];
Kiírás
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.setf(ios::scientific|ios::showpos);
153
A fenti két programrészt az alábbi kódba ágyazzuk.
char ch;
do{
switch(k){
break;
break;
default:;
154
}while(ch != 'n' && ch != 'N');
Tesztelés
Érvényes tesztesetek:
Érvénytelen tesztesetek:
155
1. Hibás méret (<=0) beírása.
156
Teljes program
#include <iostream>
#include <iomanip>
#include <vector>
#include <string>
int main()
int n;
bool error;
do{
cin >> n;
cin.clear();
}while(error);
157
// Első mátrix beolvasása
vector<double> a(n*(n+1)/2);
do{
cout << "a[" << i << "," << j << "]= ";
if(error = cin.fail()){
cin.clear();
}while(error);
158
cout << "Második mátrix:\n";
vector<double> b(n*(n+1)/2);
do{
cout << "b[" << i << "," << j << "]= ";
if(error = cin.fail()){
cin.clear();
}while(error);
// Mátrix-szorzás
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{
int k;
do{
cin >> k;
cin.clear();
160
string tmp; getline(cin,tmp);
}while(error);
switch(k){
case 1: cout.setf(ios::fixed|
ios::left|ios::showpoint);
if(i>=j)
<< c[(i-1)*i/2+j-1];
else
break;
case 2: cout.setf(ios::scientific|
ios::showpos);
161
<< c[(i-1)*i/2+j-1];
break;
default:;
return 0;
162
C++ kislexikon
természetes int n;
szám ellenőrzött
cin >> n;
beolvasása
if(cin.fail() || n<0){
exit(1);
int n;
bool error;
do{
cin >> n;
if(error){
cin.clear();
163
string tmp; getline(cin,str);
}while(error);
menü char n;
do{
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)
165
setfill(char c) kitöltő karakter
definiálása
endl sorvége
166
5. Szöveges állományok
Implementációs stratégia
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.
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
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
ofile.open(fnev.c_str())
ofstream ofile(fnev.c_str())
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()){
return 1;
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.
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.get(ch);
ifile.unsetf(ios::skipws);
char ch;
175
char ch;
ifile.get(ch);
getline(ifile, sor);
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;
istringstream is;
is.str(str);
int id;
double result;
os << "A " << 3.2 << " egy valós szám ";
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;
};
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
Specifikáció
Absztrakt program
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;
ifstream inp;
do{
string filename;
inp.clear();
180
inp.open(filenev.c_str());
if(inp.fail())
}while(inp.fail());
inp >> n;
int n;
inp >> n;
if (inp.fail() || n<1){
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);
vector<int> v(n);
if(inp.eof()) break;
if(inp.fail()){
return 1;
182
A beolvasás után a tömb elemeit kiírjuk a szabványos kimenetre.
for (int i=0; i<n-1; ++i) cout << v[i] << ", ";
Tesztelés
183
Teljes program
#include <fstream>
#include <vector>
#include <string>
int main()
string filename;
ifstream inp;
do{
inp.clear();
inp.open(filename.c_str());
if(inp.fail())
}while(inp.fail());
184
int n;
inp >> n;
if (inp.fail() || n<1){
return 1;
vector<int> v(n);
if(inp.eof()) break;
if(inp.fail()){
return 1;
for (int i=0; i<n-1; ++i) cout << v[i] << ", ";
185
// Maximum kiválasztás
for(int i=1;i<n;++i){
// Kiíratás
cout << "Ez a " << (ind+1) << ". elem." << endl;
return 0;
186
12. Feladat: Jó tanulók kiválogatása
Specifikáció
Absztrakt program
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;
};
Beolvasás
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);
getline(inp, sor);
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;
vector<Hallg> adat;
Hallg h;
while(!inp.fail()) {
190
adat.push_back(h);
Absztrakt program
Tesztelés
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>
struct Hallg{
string eha;
};
int main()
ifstream inp;
do{
string 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;
while(!inp.fail()) {
adat.push_back(h);
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;
ifstream f;
f.open("fajlnev.txt");
return 1;
ifstream f;
do{
string fnev;
f.clear();
f.open(fnev.c_str());
if(f.fail())
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);
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;
string field1;
double field4;
};
Sample v;
int a = v.field3;
ostringstream os;
197
int n; string s; double f;
198
II. RÉSZ
PROCEDURÁLIS PROGRAMOZÁ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
Implementációs stratégia
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.
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
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.
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.
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.
210
13. Feladat: Faktoriális
Specifikáció
A = ( n : ℕ, f : ℕ )
Ef = ( n=n’ )
Uf = ( n=n’ f = )
Absztrakt program
f := 1
i = 2 .. n
f := f * i
Implementálás
211
A program kerete
int f = Factorial(n);
#include <iostream>
int main()
string tmp;
char ch = 'i';
do{
212
cout << "Faktoriális = " << Factorial(
getline(cin,tmp);
return 0;
ReadNat()
main()
Factorial()
213
Absztrakt program kódolása
int Factorial(int n)
int f = 1;
f = f * i;
return f;
214
Beolvasás
int n;
do{
cin.clear();
}while(error);
return n;
215
Tesztelés
216
Teljes program
#include <iostream>
#include <string>
int main()
string tmp;
char ch = 'i';
do{
getline(cin,tmp);
return 0;
int Factorial(int n)
217
{
int f = 1;
f = f * i;
return f;
int n;
string tmp;
do{
cin.clear();
}while(error);
return n;
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
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
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
vector<int> FillVector();
220
int main()
vector<int> t = FillVector();
Selecting(t,k);
return 0;
FillVector()
main() ReadNotNullInt()
Selecting()
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;
string str;
do{
f.open( str.c_str() );
if ( error = f.fail() ){
f.clear();
}while(error);
int n;
f >> n;
vector<int> t(n);
222
for(int i=0; i<n; ++i){
f >> t[i];
f.close();
return t;
int n;
string tmp;
do{
cin >> n;
223
cin.clear();
getline(cin,tmp);
}while(error);
return n;
Kiválogatás
Tesztelés
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>
vector<int> FillVector();
int main()
vector<int> t = FillVector();
Selecting(t,k);
return 0;
226
{
int n;
string tmp;
do{
cin >> n;
cin.clear();
getline(cin,tmp);
}while(error);
return n;
vector<int> FillVector()
227
{
ifstream f;
string str;
do{
f.open( str.c_str() );
if ( error = f.fail() ){
f.clear();
}while (error);
int n;
f >> n;
vector<int> t(n);
f >> t[i];
f.close();
return t;
228
229
15. Feladat: Páros számok darabszáma
Specifikáció
n
A =(t:ℤ ,s:ℕ)
Ef = ( t=t’ )
n
Uf = ( t=t’ s= 1 )
i 1
2 t [i ]
Absztrakt program
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()
A program kerete
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
231
int main()
vector<int> t;
FillVector(t);
return 0;
int s = 0;
if(t[i]%2 == 0) ++s;
return s;
232
Tömb beolvasása
ifstream f;
string str;
do{
f.open( str.c_str() );
if ( error = f.fail() ){
f.clear();
}while (error);
int e;
f >> e;
while(!f.eof()){
233
t.push_back(e);
f >> e;
f.close();
Tesztelés
234
Teljes program
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
int main()
vector<int> t;
FillVector(t);
return 0;
int s = 0;
if(t[i]%2 == 0) ++s;
235
return s;
ifstream f;
string str;
do{
f.open( str.c_str() );
if ( error = f.fail() ){
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()
int b = …;
return 5*b;
paraméterváltozók {
eredmény- {
paraméterváltozó
…
l = true;
237
return 5*k;
238
7. Programozási tételek implementálása
Implementációs stratégia
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
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
} ++i;
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;
ind = i; }
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;
if (!felt(i)) continue;
if(l){
if(f(i)>max){
}else {
244
bool l = false;
if(f(i)>max){
struct result{
};
245
result feltmaxker(…);
246
16. Feladat: Legnagyobb osztó
Specifikáció
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
247
d := n/2
d := d - 1
,i := hamis,2
i≤
:=
k := i
i := i + 1
l
d := n/k d := 1
Implementálás
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.
ReadInt() GreaterThanOne()
main()
Divisor()
249
250
Program keret
#include <iostream>
int main()
char ch;
string tmp;
do{
251
GreaterThanOne);
return 0;
252
Beolvasás
253
bool All(int n) { return true;}
int n;
string tmp;
do{
cin >> n;
cin.clear();
getline(cin,tmp);
}while(error);
254
return n;
255
Absztrakt program kódolása
int Divisor(int n)
int d;
return d;
int Divisor(int n)
int k;
bool l = false;
l = n%i == 0;
k = i;
256
}
else return 1;
257
Tesztelés
258
Teljes program
#include <iostream>
#include <string>
#include <cmath>
int main()
char ch;
string tmp;
do{
GreaterThanOne);
259
cout << "Osztó: " << Divisor(n) << endl;
return 0;
int Divisor(int n)
int d;
return d;
bool check(int))
260
int n;
string tmp;
do{
cin >> n;
cin.clear();
getline(cin,tmp);
}while(error);
return n;
261
17. Feladat: Legkisebb adott tulajdonságú elem
Specifikáció
Absztrakt program
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
int main()
vector<int> t;
Fill(t);
NotNull);
if(CondMinSearch(t,k,min,ind))
return 0;
263
Függvények hívási láncolata
Fill()
CondMinSearch()
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 l = false;
if (t[i]%k != 1) continue;
if(l){
265
return l;
Tesztelés
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.
267
Teljes program
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
int main()
vector<int> t;
Fill(t);
NotNull);
if(CondMinSearch(t,k,min,ind))
268
cout << "A legkisebb keresett szám: " << min
return 0;
bool l = false;
if (t[i]<=0) continue;
if(l){
return l;
ifstream f;
bool hiba;
string str;
do{
269
cin >> str;
f.open( str.c_str() );
if ( hiba = f.fail() ){
f.clear();
}while (hiba);
int n;
f >> n;
t.resize(n);
f >> t[i];
f.close();
bool check(int))
int n;
270
int error = true;
string tmp;
do{
cin >> n;
cin.clear();
getline(cin,tmp);
}while(error);
return n;
271
18. Feladat: Keressünk Ibolyát
Specifikáció
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
¬l i≤n l i≤n
l := t[i] = „Ibolya” l := t[i] = „Ibolya”
i := i + 1 i := i + 1
Implementálás
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.
int main()
vector<string> t;
Fill(t);
return 0;
vector<string> t;
Fill(t);
return 0;
273
}
Fill()
main()
LinSearch() / OptLinSearch()
Beolvasás
ifstream f;
bool hiba;
string str;
do{
f.open( str.c_str() );
274
if ( hiba = f.fail() ){
f.clear();
}while (hiba);
f >> str;
while(!f.eof()){
t.push_back(str);
f >> str;
f.close();
Az első változat:
bool LinSearch(const vector<string> &t)
bool l = false;
l = "Ibolya" == t[i];
275
return l;
A második változat:
bool l = true;
l = "Ibolya" == t[i];
return l;
276
Tesztelés
277
Teljes program
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
int main()
// Adatok beolvasása
vector<string> t;
Fill(t);
// Kiértékelés
278
return 0;
bool l = false;
l = "Ibolya" == t[i];
return l;
...
279
A második program:
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
int main()
// Adatok beolvasása
vector<string> t;
Fill(t);
// Kiértékelés
return 0;
280
{
bool l = true;
l = "Ibolya" == t[i];
return l;
...
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;
if( f(i)>max){
kiválasztás int i;
int ind;
l = felt(i);
ind = i;
l = felt(i);
282
feltételes Value max;
bool l = false;
if (!felt(i)) continue;
if(l){
if(t[i]>max){
}else {
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
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
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.
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
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ó
Specifikáció
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
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
Főprogram
int main()
ReadMarks(reg);
// Lineáris keresés
293
exists = Excellent(reg[i]);
// Eredmény kiírása
return 0;
bool l = true;
294
for(int j=0; l && j<(int)v.size(); ++j){
l = 5 == v[j];
return l;
Nat()
ReadMarks() ReadInt()
main() Mark()
Excellent()
295
{
reg.resize(n);
reg[i].resize(m);
reg[i][j] = ReadInt("",
296
Lényegesen barátságosabb a program, ha a diákokra és a tantárgyakra
a nevükkel lehet hivatkozni, nem sorszámmal.
vector<string> student(n);
vector<string> subject(m);
reg.resize(n);
297
reg[i].resize(m);
reg[i][j] = ReadInt("",
Tesztelés
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).
299
Teljes program
#include <iostream>
#include <vector>
#include <string>
int main()
ReadMarks(reg);
300
// Lineáris keresés
exists = Excellent(reg[i]);
// Eredmény kiírása
return 0;
bool l = true;
l = 5 == v[j];
return l;
301
int n = ReadInt("Tanulók száma: ",
vector<string> student(n);
vector<string> subject(m);
reg.resize(n);
reg[i].resize(m);
reg[i][j] = ReadInt("",
302
"1 és 5 közötti szám!", Mark);
bool check(int))
int n;
string tmp;
do{
cin >> n;
cin.clear();
getline(cin,tmp);
}while(error);
return n;
303
20. Feladat: Azonos színű oldalak
Specifikáció
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:ℕ
e := e + 1 SKIP
Implementálás
305
ReadInt() G2()
main() ReadPoligon()
MaximalFittness() IdenticalEdges()
Főprogram
int main()
// Sokszögek beolvasása
306
cout << "Első sokszög oldalainak színei:\n";
ReadPoligon(x);
ReadPoligon(y);
// Eredmény kiírása
<< MaximalFittness(x,y)
return 0;
int ind = 0;
307
int c = IdenticalEdges(x,y,i);
max = c; ind = i;
return ind;
int i)
int n = (int)x.size();
int c = 0;
return c;
Sokszög beolvasása
308
A ReadPoligon() függvény sztringként olvassa be egy sokszög oldalainak
színét.
cout << "Az " << i+1 << " oldal színe:";
Tesztelés
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>
int main()
//Sokszögek beolvasása
311
ReadPoligon(x);
ReadPoligon(y);
//Eredmény kiírása
<< MaximalFittness(x,y)
return 0;
int ind = 0;
int c = IdenticalEdges(x,y,i);
max = c; ind = i;
return ind;
312
int IdenticalEdges(const vector<string> &x,
int i)
int =(int)x.size();
int c = 0;
return c;
cout << "Az " << i+1 << " oldal színe:";
int n;
string tmp;
313
do{
cin >> n;
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 = ( 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
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
Főprogram
int main()
vector<vector<int> > t;
ReadMatrix(t);
int ind;
if(LinSearch(t,ind))
else
316
return 0;
ReadMatrix()
main()
LinSearch() Summation()
int n = (int)t.size();
bool l = true;
317
l = 0 == Summation(t,k);
return l;
int n = (int)t.size();
int s = 0;
s += t[(abs(k)-k+2*i)/2][(abs(k)+k+2*i)/2];
return s;
318
{
ifstream f;
string fname;
bool hiba;
do{
f.open(fname.c_str());
if(hiba = f.fail()){
f.clear();
}while(hiba);
int n;
f >> n;
t.resize(n);
t[i].resize(n);
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.
Negativ_Matrix_Size,
Not_Enough_Number };
vector<vector<int> > t;
try{
ReadMatrix(t);
}catch(Errors ex){
switch(ex){
320
case Non_Integer:
case Negativ_Matrix_Size:
break;
case Not_Enough_Number:
break;
default:;
exit(1);
ifstream f;
bool error;
string str;
321
do{
f.open( str.c_str() );
if ( error = f.fail() ){
f.clear();
}while (error);
int n = ReadIntFromFile(f);
t.resize(n);
t[i].resize(n);
t[i][j] = ReadIntFromFile(f);
f.close();
322
automatikusan tovább dobja, így ezek végül a main függvény
kivételkezelésében csapódnak le.
string str;
f >> str;
int n = atoi(str.c_str());
return n;
Tesztelés
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>
Negativ_Matrix_Size,
Not_Enough_Number };
int main()
vector<vector<int> > t;
try{
ReadMatrix(t);
325
}catch(Errors ex){
switch(ex){
case Non_Integer:
case Negativ_Matrix_Size:
break;
case Not_Enough_Number:
break;
default:;
exit(1);
// Eredmény kiírása
int ind;
if(LinSearch(t,ind))
else
return 0;
326
{
int n = (int)t.size();
bool l = false;
l = 0 == Summation(t,k);
return l;
int n = (int)t.size();
int s = 0;
s += t[(abs(k)-k+2*i)/2][(abs(k)+k+2*i)/2];
return s;
ifstream f;
bool error;
string str;
do{
327
cout << "Fajl neve:";
f.open( str.c_str() );
if ( error = f.fail() ){
f.clear();
}while (error);
int n = ReadIntFromFile(f);
t.resize(n);
t[i].resize(n);
t[i][j] = ReadIntFromFile(f);
f.close();
328
string str;
f >> str;
int n = atoi(str.c_str());
return n;
329
C++ kislexikon
függvény
kivétel figyelése try{
kezelése if(kivétel==változó) …
330
9. Fordítási egységekre bontott program
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
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.
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
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
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
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ő.
p[i]:=pont(i) t[i,j]>maxi
Implementálás
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
int main()
vector<vector<double> > t;
Fill(t);
if(t.size()<1 or t[i].size()<3){
return 1;
Write(t);
vector<double> p(t.size());
double max;
Init(t,p,max);
Select(p,max);
339
return 0;
ReadIntFromFile()
Fill()
ReadRealFromFile()
Write()
main()
Init() Result()
Select()
Komponens szerkezet
340
main.cpp matrix.h - matrix.cpp
main() Fill()
Init() Write()
Select() ReadIntFromFile
Result() ReadRealFromFile
p[0] = Result(t[0]);
max = p[0];
p[i] = Result(t[i]);
341
}
o = o + v[j];
return (o-maxi-mini)/(v.size()-2);
342
if(p[i] == max) cout << i+1 << "\t";
Mátrix csomag
#ifndef MATRIX_H
#define MATRIX_H
#include <vector>
void Write(
#endif
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.
ifstream f;
bool hiba;
string str;
do{
f.open( str.c_str() );
if ( hiba = f.fail() ){
f.clear();
}while (hiba);
344
int n, m;
f >> n >> m;
t.resize(n);
t[i].resize(m);
f.close();
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.
Negativ_Matrix_Size, Not_Enough_Number};
struct Exceptions{
Errors code;
std::string msg;
};
ifstream f;
bool hiba;
string str;
do{
346
cin >> str;
f.open( str.c_str() );
if ( hiba = f.fail() ){
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){
347
Exceptions ex;
ex.code = Negativ_Matrix_Size;
ostringstream ss;
ss << m;
ex.msg = ss.str();
throw ex;
t.resize(n);
t[i].resize(m);
t[i][j] = ReadRealFromFile(f);
f.close();
348
{
string str;
f >> str;
if( f.eof() ){
Exceptions ex;
ex.code = Not_Enough_Number;
throw ex;
int n = atoi(str.c_str());
Exceptions ex;
ex.code = Non_Integer;
ex.msg = str;
throw ex;
return n;
349
string str;
f >> str;
if( f.eof() ){
Exceptions ex;
ex.code = Not_Enough_Number;
throw ex;
double a = atof(str.c_str());
Exceptions ex;
ex.code = Non_Real;
ex.msg = str;
throw ex;
return a;
vector<vector<double> > t;
try{
Fill(t);
350
}catch(Exceptions ex){
switch(ex.code){
case Non_Integer:
case Non_Real:
case Negativ_Matrix_Size:
case Not_Enough_Number:
default:;
exit(1);
if(t.size()<1){
if(t[0].size()<3){
351
A csomag másik eleme, a Write(), egy valós számokat tartalmazó
mátrixot ír ki.
Tesztelés
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"
int main()
vector<vector<double> > t;
try{
Fill(t);
}catch(Exceptions ex){
switch(ex.code){
case Non_Integer:
355
<< ex.msg << endl; break;
case Non_Real:
case Negativ_Matrix_Size:
case Not_Enough_Number:
default:;
exit(1);
if(t.size()<1){
if(t[0].size()<3){
356
Write(t);
vector<double> p(t.size());
double max;
Init(t,p,max);
Select(p,max);
return 0;
p[0] = Result(t[0]);
max = p[0];
p[i] = Result(t[i]);
357
}
o = o + v[j];
return (o-maxi-mini)/(v.size()-2);
358
matrix.h:
#ifndef MATRIX_H
#define MATRIX_H
#include <vector>
#include <string>
Negativ_Matrix_Size, Not_Enough_Number};
struct Exceptions{
Errors code;
std::string msg;
};
void Write(
#endif
matrix.cpp:
#include "matrix.h"
#include <iostream>
#include <cstdlib>
#include <fstream>
359
#include <sstream>
ifstream f;
bool hiba;
360
string str;
do{
f.open( str.c_str() );
if ( hiba = f.fail() ){
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);
t[i].resize(m);
t[i][j] = ReadRealFromFile(f);
f.close();
string str;
f >> str;
if( f.eof() ){
362
Exceptions ex;
ex.code = Not_Enough_Number;
throw ex;
int n = atoi(str.c_str());
Exceptions ex;
ex.code = Non_Integer;
ex.msg = str;
throw ex;
return n;
string str;
f >> str;
if( f.eof() ){
Exceptions ex;
ex.code = Not_Enough_Number;
throw ex;
363
double a = atof(str.c_str());
Exceptions ex;
ex.code = Non_Real;
ex.msg = str;
throw ex;
return a;
364
23. Feladat: Melyikből hány van
Specifikáció
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
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
Store()
main()
Write()
367
Főprogram
int main()
int n;
Store(n);
Write();
return 0;
Tároló csomag
struct Pair{
int value;
int no;
368
};
static vector<Pair> s;
void Store(int e)
bool l = false;
int ind;
l = s[i].value == e;
ind = i;
if(l) ++s[ind].no;
else{
s.push_back(p);
369
}
void Write()
Tesztelés
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"
int main()
int n;
Store(n);
Write();
371
return 0;
372
container.h:
#ifndef CONTAINER_H
#define CONTAINER_H
#include <vector>
void Write();
#endif
container.cpp:
#include "container.h"
#include <iostream>
struct Pair{
int value;
int no;
};
static vector<Pair> s;
373
void Store(int e)
bool l = false;
int ind;
l = s[i].value == e;
ind = i;
if(l) ++s[ind].no;
else{
s.push_back(p);
void Write()
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
375
10. Rekurzív programok kódolása
Implementációs stratégia
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
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
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ó
Specifikáció
A = ( n,k,b : ℕ )
Ef = ( n=n’ k=k’ k [0..n] )
Uf = ( n=n’ k=k’ b= )
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
b := Binomial(n,k)
k=0
b := 1 b := Binomial(n,k–1)* (n–k+1)/k
b := Binomial(n,k)
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
ReadInt()
main() Nat()
Binomial()
382
10-2. ábra. Alprogramok hívási lánca
383
A fő program
int main()
char ch;
do{
return 0;
Rekurzív függvény
384
if(0 == n || 0 == k || k == n) return 1;
385
Tesztelés
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"
int main()
char ch;
do{
return 0;
387
int Binomial(int n, int k)
if(0 == n || 0 == k || k == n) return 1;
read.h:
#ifndef READ_H
#define READ_H
#include <string>
std::string errormsg,
#endif
read.cpp:
#include "read.h"
#include <iostream>
388
using namespace std;
int n;
string tmp;
do{
cin >> n;
if(cin.fail() || !check(n)){
cin.clear();
getline(cin,tmp);
}while(hiba);
return n;
389
25. Feladat: Hanoi tornyai
Specifikáció
A = ( n : ℕ , ss : ( ℕ×ℕ )* )
Ef = ( n=n’ )
Uf = ( n=n’ ss = Hanoi(n,1,3,2) )
Absztrakt program
390
Implementálás
ReadInt()
main() Nat()
Hanoi()
10-3. ábra. Alprogramok hívási láncai
A fő program
int main()
char ch;
do{
391
cout << Hanoi(n,1,3,2) << endl;
return 0;
Rekurzív függvény
ostringstream ss;
<< Hanoi(n-1,k,j,i);
return ss.str();
Tesztelés
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"
int main()
char ch;
do{
return 0;
394
ostringstream ss;
<< Hanoi(n-1,k,j,i);
return ss.str();
read.h:
#ifndef READ_H
#define READ_H
#include <string>
#endif
read.cpp:
#include "read.h"
#include <iostream>
395
bool ci(int k){ return true;}
int n;
string tmp;
do{
cin >> n;
if(cin.fail() || !cond(n)){
cin.clear();
getline(cin,tmp);
}while(hiba);
return n;
396
26. Feladat: Quick sort
Specifikáció
A = ( v : ℤk )
Ef = ( v=v’ )
Uf = ( v=rendezett(v’) )
Absztrakt program
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
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
main() Read()
Quick()
Divide() Write()
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);
Write(v);
Quick(v,0,(int)v.size()-1);
Write(v);
return 0;
Rekurzív eljárás
if( m < n ){
int a = Divide(v,m,n);
Quick(v,m,a-1);
401
Quick(v,a+1,n);
int x = v[f];
while(a<f){
v[a] = x;
return a;
402
Tömb műveletek
ifstream f("input.txt");
if (f.fail()) {
exit(1);
};
int n;
f >> n;
t.resize(n);
f >> t[i];
403
void Write(const vector<int> &t)
Tesztelés
404
Teljes program
main.cpp:
#include <iostream>
#include "array.h"
int main()
vector<int> v;
Read(v);
Write(v);
Quick(v,0,(int)v.size()-1);
Write(v);
return 0;
405
}
if( m < n )
int a = Divide(v,m,n);
Quick(v,m,a-1);
Quick(v,a+1,n);
int x = v[f];
while(a<f){
v[a] = x;
return a;
array.h:
406
#ifndef ARRAY_H
#define ARRAY_H
#include <vector>
#endif
array.cpp:
#include "array.h"
#include <fstream>
#include <iostream>
ifstream f("input.txt");
if (f.fail()) {
exit(1);
};
407
int n;
f >> n;
t.resize(n);
f >> t[i];
408
III. RÉSZ
PROGRAMOZÁS OSZTÁLYOKKAL
410
11. A típus megvalósítás eszköze: az osztály
Implementációs stratégia
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.
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 {
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
class Tipus {
private:
// adattagok
Tipus11 tag1;
Tipus12 tag2;
414
…
public:
// konstruktorok
Tipus();
Tipus(…);
// destruktor
~Tipus();
// metódusok deklarációi
Tipus21 Metod1(…);
Tipus22 Metod2(…);
};
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
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(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) { … }
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
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
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.
424
425
Függvények hívási láncolata
ReadReal() Pos()
ReadInt()
Point()
main()
Set()
Sphere()
In() Distance()
class Sphere{
private:
Point c;
double r;
public:
c=p; r=a;
426
}
{ return c.Distance(p)<=r; }
};
class Point{
private:
double x,y,z;
public:
Point(){ x = y = z = 0.0;}
{ x = a; y = b; z = c;}
427
return sqrt(pow(p.x-x,2)+ pow(p.y-y,2)
+ pow(p.z-z,2));
};
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;
Point c; c.Set(x,y,z);
Sphere g(c,r);
429
vector<Point> v(n);
v[i].Set(x,y,z);
int db = 0;
if(g.In(v[i])) ++db;
430
Tesztelés
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"
int main()
// Űrállomás beolvasása
double x,y,z,r;
Point c; c.Set(x,y,z);
Sphere g(c,r);
// UFO-k beolvasása
433
"Természetes számot várok!");
vector<Point> v(n);
v[i].Set(x,y,z);
// Számlálás
int db = 0;
if(g.In(v[i])) ++db;
// Kiírás
return 0;
sphere.h:
#ifndef SPHERE_H
#define SPHERE_H
434
#include "point.h"
class Sphere{
private:
Point c;
double r;
public:
{ 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;}
{ x = a; y = b; z = c;}
+ pow(p.z-z,2));
};
#endif
436
read.h:
#ifndef _READ_H
#define _READ_H
#include <string>
std::string errormsg,
#endif
read.cpp:
#include "read.h"
#include <iostream>
bool check(int) )
int n;
do{
437
if(error = cin.fail() || !check(n)){
cin.clear();
}while(error);
return n;
bool check(double))
double a;
do{
cin.clear();
}while(error);
return a;
438
28. Feladat: Zsák
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.
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
439
b :=
i =1..n
b := b {t[i]}
e := MAX(b)
Implementálás
Komponens szerkezet
Bag
• main() • Bag()
• Read() • Put()
• Max()
11-5. ábra. Komponens szerkezet
440
Read()
Bag()
main()
Put()
Max()
class Bag{
public:
Bag();
private:
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"
void Bag::Put(int e)
++v[e];
int e = 0;
442
if (v[k]> max){ max = v[k]; e = k; }
return e;
Főprogram kódolása
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)
catch(Bag::Errors ex){
if(Bag::EmptyBag == ex)
444
Tesztelés
445
Teljes program
main.cpp:
#include <iostream>
#include <fstream>
#include <vector>
#include <cstdlib>
#include "bag.h"
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)
446
try{ cout << "Leggyakoribb elem: " << b.Max();}
catch(Bag::Errors ex){
if(Bag::EmptyBag == ex)
return 0;
ifstream f("input.txt");
if (f.fail()) {
exit(1);
};
int n;
f >> n;
t.resize(n);
f >> t[i];
bag.h:
#ifndef BAG_H
447
#define BAG_H
#include <vector>
class Bag{
public:
Bag();
private:
int v[n];
};
#endif
bag.cpp:
#include "bag.h"
448
Bag::Bag()
void Bag::Put(int e)
++v[e];
int e = 0;
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 = ( 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: ℝ
450
Absztrakt program
s := nullvektor
i =1 .. n
s := s + t[i]
l := s*v=0.0
Implementálás
Komponens szerkezet
451
vector2d.h -
main.cpp read.h - read.cpp
vector2d.cpp
ReadInt() Nat()
Vector2D()
ReadReal()
Fill()
SetX(),
SetY()
main() Vector2D()
Sum()
operator+=()
ReadReal()
Vector2D(,)
operator*()
452
Főprogram kódolása
vector<Vector2D> t;
Fill(t);
Vector2D s = Sum(t);
453
{
t[i].SetX(ReadReal("x = "));
t[i].SetY(ReadReal("y = "));
Vector2D s;
s+=t[i];
return s;
class Vector2D{
454
private:
double x,y;
public:
Vector2D():x(0),y(0){}
};
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.
x+=v.x; y+=v.y;
return *this;
Tesztelés
456
1. Konstruktor teszt. (Létrejön-e a nullvektor?)
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"
int main()
vector<Vector2D> t;
Fill(t);
Vector2D s = Sum(t);
ReadReal("y = ",""));
459
else cout << "Nem merőleges.";
return 0;
t[i].SetX(ReadReal("x = ",""));
t[i].SetY(ReadReal("y = ",""));
Vector2D s;
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){}
};
#endif
vector2d.cpp:
#include "vector2D.h"
x+=v.x; y+=v.y;
return *this;
461
double operator*( const Vector2D& v1,
read.h:
#ifndef READ_H
#define READ_H
#include <string>
#endif
read.cpp:
#include "read.h"
462
#include <iostream>
bool check(int) )
int n;
do{
if(cin.fail() || !check(n)){
cin.clear();
getline(cin,tmp);
}while(error);
return n;
463
bool check(double))
double a;
do{
a = atof(str.c_str());
}while(error);
return a;
464
C++ kislexikon
osztály class{
private:
int i;
public:
Tipus();
Tipus(…);
~Tipus();
void method1(…);
void method3(…){ … }
};
Tipus::Tipus(){…}
Tipus::Tipus(int a):a(i){…}
Tipus::~Tipus(){ … }
void Tipus::method1(…){…}
void method4(…){…}
465
láthatóság private , public
konstruktor Tipus();
Tipus(…);
értékadás
destruktor ~Tipus();
hívás t.method(…);
metódus
inline void method3(…) { … }
466
12. Felsorolók típusainak megvalósítása
Implementációs stratégia
t.First()
t.End()
feldolgozás(t.Current())
t.Next()
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.
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
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
f >> e;
while( !f.fail()){
feldolgozás(e);
f >> e;
470
12-4. Előre-olvasási technika egy C++ nyelvi változata
ofstream y("out.txt");
char ch;
vagy
ifstream x("inp.txt");
ofstream y("out.txt");
char ch;
x.unsetf(ios::skipws);
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);
int n, db = 0;
if(n%2 == 0) ++db;
vagy
ifstream x("inp.txt");
int n, db = 0;
472
if(n%2 == 0) ++db;
string str;
db = hossz = 0;
hossz += str.size();
++db;
vagy
ifstream x("inp.txt");
string str;
db = hossz = 0;
hossz += str.size();
++db;
473
int atl = hossz/db;
int szam;
string nev;
vagy
ifstream x("inp.txt");
int szam;
string nev;
474
ifstream x("inp.txt");
string sor;
for(getline(x,sor); !x.fail();getline(x,sor)){
...
vagy
ifstream x("inp.txt");
string sor;
while(getline(x,sor)){
...
475
30. Feladat: Könyvtár
Specifikáció
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
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
Read()
main()
Write()
477
Első megoldás kódja
struct Book{
int id;
string author;
string title;
string publisher;
string year;
int piece;
string isbn;
};
int main()
ifstream x("inp.txt");
if (x.fail() ) {
478
char ch; cin>>ch; exit(1);
ofstream y("out.txt");
if (y.fail() ) {
Book dx;
Status sx;
if (0 == dx.count) {
return 0;
string sor;
479
getline(x,sor,'\n');
if (!x.fail()) {
sx = norm;
dx.title = sor.substr(21,19);
dx.publisher = sor.substr(42,14);
dx.isbn = sor.substr(67,14);
else sx = abnorm;
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.
Stock()
Read()
~Stock()
main()
Result()
Write()
~Result()
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);
private:
std::ifstream f;
};
if ( fname.size()<1 ) {
482
}
f.open(fname.c_str());
if ( f.fail() ){
string sor;
getline(f, sor,'\n');
if (!f.fail()) {
sf = norm;
483
df.author = sor.substr( 5,14);
df.title = sor.substr(21,19);
df.publisher = sor.substr(42,14);
df.isbn = sor.substr(67,14);
else sf = abnorm;
class Result{
public:
Result(std::string fname);
private:
std::ofstream f;
};
484
Result::Result(string fname = "")
if ( fname.size()<1 ) {
485
f.open(fname.c_str());
if ( f.fail() ){
int main()
486
{
Stock x("inp.txt");
Result y("out.txt");
Book dx;
Status sx;
return 0;
Tesztelés
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
#include <iostream>
#include <iomanip>
#include <string>
struct Book{
int id;
string author;
string title;
string publisher;
string year;
int piece;
string isbn;
};
489
int main()
ifstream x("inp.txt");
if (x.fail() ) {
ofstream y("out.txt");
if (y.fail() ) {
Book dx;
Status sx;
if (0 == dx.piece) {
return 0;
490
string sor;
getline(x,sor,'\n');
if (!x.fail()) {
sx = norm;
dx.title = sor.substr(21,19);
dx.publisher = sor.substr(42,14);
dx.isbn = sor.substr(67,14);
else sx = abnorm;
491
A második változat teljes kódja:
main.cpp:
#include <fstream>
#include <string>
#include "stock.h"
#include "result.h"
int main()
Stock x("inp.txt");
Result y("out.txt");
Book dx;
Status sx;
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;
};
class Stock{
public:
Stock(std::string fname);
private:
std::ifstream f;
};
#endif
stock.cpp:
#include "stock.h"
493
#include <iostream>
#include <cstdlib>
if ( fname.size()<1 ) {
f.open(fname.c_str());
if ( f.fail() ){
exit(2);
string sor;
getline(f, sor,'\n');
if (!f.fail()) {
494
sf = norm;
df.title = sor.substr(21,19);
df.publisher = sor.substr(42,14);
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);
private:
std::ofstream f;
};
#endif
result.cpp:
#include "result.h"
#include <iostream>
#include <cstdlib>
#include <iomanip>
496
Result::Result(string fname = "")
if ( fname.size()<1 ) {
f.open(fname.c_str());
if ( f.fail() ){
exit(2);
497
31. Feladat: Havi átlag-hőmérséklet
Specifikáció
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
499
havi:=össz/db
x.Current() ~ havi
x.End() ~ xvége
Absztrakt program
darab := 0
t.First()
t.End()
t.Current().akt=t.Current().elő
darab:=darab+1 SKIP
t.Next()
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
Implementálás
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()
Főprogram kódolása
int main()
502
Pair_Enor t("input.txt");
int count = 0;
if(t.Current().curr == t.Current().prev)
++count;
<< count;
return 0;
Pair_Enor()
First()
main() Next()
End()
Current()
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:
{ x.Open(str); }
void First();
void Next();
};
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()
First()
Next()
First()
End()
Current()
Next()
Next() End()
Current()
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
struct Day{
int month;
double term;
};
class Month_Average_Enor{
private:
std::ifstream f;
Day day;
Status st;
507
bool end;
double avrterm;
void Read();
public:
void Next();
};
void Month_Average_Enor::Next()
if(!end){
avrterm = 0.0;
int c = 0;
508
avrterm += day.term;
++c;
avrterm /= c;
f.open(str.c_str());
if(f.fail()){
exit(1);
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
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"
int main()
Pair_Enor t("input.txt");
int count = 0;
if(t.Current().curr == t.Current().prev)
++count;
<< 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:
{ x.Open(str); }
void First();
void Next();
};
#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>
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 Next();
516
double Current() const { return avrterm; }
};
#endif
month_average_enor.cpp:
#include "month_average_enor.h"
#include <iostream>
#include <cstdlib>
f.open(str.c_str());
if(f.fail()){
exit(1);
517
void Month_Average_Enor::Next()
if(!end){
avrterm = 0.0;
int db = 0;
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
Specifikáció
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
t.End()
bek:=t.Current()
t.Next()
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)
522
(sor-beli szavak száma)
st, sor, f : read
Implementálás
523
main.cpp enor.h-enor.cpp
class Enor
• main() • Enor()
• First()
• Next()
• End()
• Current()
• ~Enor()
Főprogram kódolása
int main()
Enor t("input.txt");
int ind;
double max;
bool l = false;
double rate
= (double)bek.word/(double)bek.line;
524
max = rate;
ind = bek.no;
} else {
l = true;
max = rate;
ind = bek.no;
return 0;
525
Enor()
First()
Next()
main()
End()
Current
Current()
Enor osztály
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();
public:
void First()
void Next();
527
Statistic Current() const { return current;}
};
void Enor::Next()
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;
++current.word;
current.apple = current.apple ||
w.find("alma")!=string::npos;
f.open(str.c_str());
if(f.fail()){
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
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"
int main()
Enor t("input.txt");
int ind;
double max;
bool l = false;
double rate
= (double)bek.word/(double)bek.line;
max = rate;
ind = bek.no;
else {
l = true;
533
max = rate;
ind = bek.no;
return 0;
534
enor.h:
#ifndef ENOR_H
#define ENOR_H
#include <fstream>
#include <string>
#include <sstream>
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:
void First()
void Next();
};
#endif
536
enor.cpp:
#include "enor.h"
#include <iostream>
#include <cstdlib>
f.open(str.c_str());
if(f.fail()){
exit(1);
void Enor::Next()
if(!end){
++current.no;
current.word = current.line = 0;
current.apple = false;
537
Read()){
++current.line;
string 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
ifstream x("inp.txt");
char ch;
x.unsetf(ios::skipws);
...
állomány int n, db = 0;
számainak
for(x >> n; !x.fail(); x >> n){
olvasása
...
!x.fail();
x>>szam >>nev){
539
...
atoi(sor.substr( 0, 4).c_str());
...
540
13. Dinamikus szerkezetű típusok osztályai
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
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)
h nil
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.
Nyelvi elemek
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
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;
v int* cím
cím 0 1 2 3
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.
0 1 2 3
cím 10 11 12 13
20 21 22 23
w= new int*[3];
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;
int value;
Node *next;
value(i), next(q){}
};
550
Node *p = new Node();
u->next = p;
if(p != NULL){
u->next = p->next;
delete p;
551
}
Node *p = h;
h = h->next;
delete p;
Node *u = h; Node *u = h;
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;
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
Specifikáció
556
Absztrakt program
cin >> e
cin.fail()
s.Push (e)
cin >> e
s.Empty()
s.Pop()
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
main.cpp stack.h-stack.cpp
class Stack
• Stack()
• ~Stack()
main() • Push()
• Pop()
• Top()
• Empty()
Főprogram kódolása
...
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ó.
int i;
try{ s.Push(i);}
if (Stack::FULLSTACK == e)
560
while(!s.Empty()){
561
Verem típus tömbös megvalósítása
class Stack{
public:
Stack();
Stack(int s);
~Stack();
int Pop();
bool Empty()const;
private:
Stack(const Stack&);
562
void Allocate(int n);
int size;
int* vect;
int top;
};
Stack::Stack() { Allocate(10); }
Stack::Stack(int n) { Allocate(n); }
563
void Stack::Allocate(int n)
try{
size = n;
top = -1;
void Stack::Push(int e)
vect[++top]=e;
564
int Stack::Pop()
return vect[top--];
return vect[top];
bool Stack::Empty()const
return -1 == top;
class Stack{
public:
565
Stack();
~Stack();
Stack(const Stack&);
int Pop();
bool Empty()const;
private:
struct Node{
int val;
Node *next;
};
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;
void Stack::Push(int e)
try{
}catch(std::bad_alloc o){
567
throw FULLSTACK;
int Stack::Pop()
int e = head->val;
Node *p = head;
head = head->next;
delete p;
return e;
int Stack::Top()const
568
return head->val;
bool Stack::Empty()const
Stack::Stack(const Stack& s)
else {
try{
}catch(std::bad_alloc o){
throw FULLSTACK;
Node *q = head;
Node *p = s.head->next;
while(p != NULL){
569
catch(std::bad_alloc o){throw FULLSTACK;}
q = q->next;
p = p->next;
Node *p;
while(head != NULL){
p = head;
head = head->next;
delete p;
570
else {
Node *q = head;
Node *p = s.head->next;
while(p != NULL){
q = q->next;
p = p->next;
return *this;
571
Tesztelés
Stack s;
try{ s.Top();}
if (Stack::EMPTYSTACK == e)
try{
int n;
while(true){
Stack s(n);
572
if (Stack::FULLSTACK == e)
573
Stack s;
int i;
try{
while(true){ s.Push(i++); }
if (Stack::FULLSTACK == e)
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"
int main()
Stack s;
int i;
try{ s.Push(i);}
if (Stack::FULLSTACK == e)
while(!s.Empty()){
return 0;
576
#ifndef STACK_H
#define STACK_H
class Stack{
public:
Stack();
Stack(int s);
~Stack();
int Pop();
bool Empty()const;
private:
Stack(const Stack&);
int size;
int* vect;
577
int top;
};
#endif
#include <memory>
Stack::Stack() { Allocate(10); }
Stack::Stack(int n) { Allocate(n); }
void Stack::Allocate(int n)
try{
size = n;
top = -1;
Stack::~Stack()
delete[] vect;
void Stack::Push(int e)
578
{
vect[++top]=e;
int Stack::Pop()
return vect[top--];
return vect[top];
bool Stack::Empty()const
return -1 == top;
#define STACK_H
class Stack{
public:
579
enum Exceptions{EMPTYSTACK, FULLSTACK};
Stack();
~Stack();
Stack(const Stack&);
int Pop();
bool Empty()const;
private:
struct Node{
int val;
Node *next;
};
Node *head;
};
#endif
580
581
stack.cpp: (láncolt listás változat)
#include "stack.h"
#include <memory>
Stack::Stack(): head(NULL){}
Stack::~Stack()
Node *p;
while(head != NULL){
p = head;
head = head->next;
delete p;
void Stack::Push(int e)
int Stack::Pop()
int e = head->val;
582
Node *p = head;
head = head->next;
delete p;
return e;
int Stack::Top()const
return head->val;
bool Stack::Empty()const
583
Stack::Stack(const Stack& s)
else {
try{
}catch(std::bad_alloc o){
throw FULLSTACK;
Node *q = head;
Node *p = s.head->next;
while(p != NULL){
q = q->next;
p = p->next;
584
Node *p;
while(head != NULL){
p = head;
head = head->next;
delete p;
else {
Node *q = head;
Node *p = s.head->next;
while(p != NULL){
q = q->next;
p = p->next;
return *this;
585
34. Feladat: Kettős sor
Specifikáció
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
587
Absztrakt program
cin >> e
cin.fail()
e<0
b.Loext(e) b.hiext(e)
cin >> e
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()
Implementálás
589
main.cpp biqueue.h-biqueue.cpp biqueue.h
Főprogram kódolása
BiQueue x;
int i;
if (i>0) x.Hiext(i);
else x.Loext(i);
590
}
i = it1.Current();
int s = 0;
BiQueue::Enumerator it2
= x.CreateEnumerator ();
if (it2.Current() == i) ++s;
return 0;
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();
int Lopop();
int Hipop();
BiQueue(const BiQueue&);
private:
struct Node{
int val;
Node *next;
592
Node *prev;
};
Node *first;
Node *last;
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:
private:
BiQueue *bq;
Node *current;
};
Enumerator it = *this;
current = current->next;
594
return it;}
current = current->next;
return *this;}
Enumerator CreateEnumerator()
{ return Enumerator(this); }
BiQueue::~BiQueue(){
q = first;
595
while( q != NULL){
p = q;
q = q->next;
delete p;
if(NULL == b.first){
}else{
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){
// destruktor
// másoló konstruktor
return *this;
Node *p = first;
while(p != NULL){
Node *q = p->next;
delete p;
p = q;
if(NULL == s.first){
}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;
first = p;
598
Node *p = new Node(e,NULL,last);
last = p;
int BiQueue::Lopop(){
int e = first->val;
Node *p = first;
first = first->next;
delete p;
return e;
int BiQueue::Hipop(){
int e = last->val;
Node *p = last;
599
last = last->prev;
delete p;
return e;
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
601
Teljes program
main.cpp:
#include <iostream>
#include "biqueue.h"
int main()
BiQueue x;
int i;
if (i>0) x.Hiext(i);
else x.Loext(i);
i = it1.Current();
int s = 0;
= x.CreateEnumerator ();
if (it2.Current() == i) ++s;
602
}
return 0;
603
biqueue.h:
#ifndef BIQUEUE_H
#define BIQUEUE_H
#include <memory>
class BiQueue{
public:
BiQueue():
first(NULL),last(NULL),enumeratorCount (0){}
BiQueue(const BiQueue&);
~BiQueue();
int Lopop();
int Hipop();
private:
struct Node{
int val;
Node *next;
Node *prev;
604
};
Node *first;
Node *last;
int enumeratorCount;
public:
class Enumerator{
public:
Enumerator(BiQueue *p):bq(p),current(NULL)
{++(bq->enumeratorCount);}
~Enumerator(){--(bq->enumeratorCount);}
private:
BiQueue *bq;
Node *current;
};
Enumerator CreateEnumerator()
{return Enumerator(this);}
};
#endif
biqueue.cpp:
605
#include "biqueue.h"
BiQueue::~BiQueue(){
q = first;
while( q != NULL){
p = q;
q = q->next;
delete p;
else{
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
}
Node *p = first;
while(p != NULL){
Node *q = p->next;
delete p;
p = q;
else{
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){
first = p;
int BiQueue::Lopop(){
int e = first->val;
Node *p = first;
first = first->next;
delete p;
return e;
last = p;
int BiQueue::Hipop(){
608
if(enumeratorCount != 0) throw UNDERTRAVERSAL;
int e = last->val;
Node *p = last;
last = last->prev;
delete p;
return e;
609
C++ kislexikon
pointer int* p;
delete[] t;
Node *next;
:value(i), next(q){}
};
u->next = p;
u->next = p->next;
delete p;
610
}
Node *u = h; Node *u = h;
Node *p Node *p
u->next=p; u->next=p;
u = p; u = p;
} }
lista while(h!=NULL){
lebontása Node *p = h;
h = p->next;
delete p;
// destruktor törzse
return *this;
611
14. Objektum-orientált kód-újrafelhasználási technikák
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
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
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
622
T szivacs
10 0210201012
623
Specifikáció
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.
homok -2 -
fű +1 -
mocsár -1 fű
homok +3 -
fű -2 homok
mocsár -4 fű
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
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()
627
Implementálás
Főprogram kódolása
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);
char l;
string a;
f >> l >> a;
switch(l){
break;
break;
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);
creatures[i]->Transmute(palya[j]);
if (creatures[i]->Alive())
delete creatures[i];
630
}
Creature osztály
class Creature {
protected:
std::string name;
int power;
Creature(std::string a):name(a) {}
public:
virtual ~Creature(){}
};
631
Speciális lények osztályai
public:
Greenfinch(std::string a):Creature(a){power=10;}
};
public:
};
public:
};
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.
switch(gound){
default:;
switch(gound){
default:;
633
void Squelchy::Transmute(int &gound)
switch(gound){
default:;
Tesztelés
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"
int main()
ifstream f("input.txt");
int n;
f >> n;
vector<Creature*> creatures(n);
char l;
string a;
f >> l >> a;
switch(l){
break;
636
break;
break;
default:;
int m;
f >> m;
vector<int> palya(m);
++j){
creatures[i]->Transmute(palya[j]);
if (creatures[i]->Alive())
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:
virtual ~Creature(){}
};
public:
Greenfinch(std::string a):Creature(a){power=10;}
};
638
class Sandbug : public Creature {
public:
};
public:
};
#endif
639
creature.cpp:
#include "creature.h"
switch(gound){
default:;
switch(gound){
default:;
640
void Squelchy::Transmute(int &gound)
switch(gound){
default:;
641
36. Feladat: Lengyel forma és kiértékelése
Specifikáció
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
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())
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()
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
Tokenek osztályai
class Token{
Token*&);
public:
class IllegalElementException{
private:
char ch;
public:
IllegalElementException(char c) : ch(c){}
646
char Message() const { return ch;}
};
virtual ~Token(){}
};
public:
};
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.
char ch;
s >> ch;
switch(ch){
s.putback(ch);
int intval;
s >> intval;
t = new Operand(intval);
break;
Token::IllegalElementException(ch);
return s;
648
}
Tárolók osztály-sablonjai
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
BiQueue<Token*> x;
try{
Token *t;
cin >> t;
while(!t->Is_End()){
x.Hiext(t);
cin >> t;
}catch(Token::IllegalElementException *ex){
650
<< ex->Message() << endl;
delete ex;
DeallocateToken(x);
exit(1);
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
652
Teljes program
main.cpp:
#include <iostream>
#include <cstdlib>
#include „token.h”
#include „stack.hpp”
#include „biqueue.hpp”
int main()
// 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){
654
// Lengyel formára hozás
BiQueue<Token*> y;
Stack<Token*> s;
BiQueue<Token*>::Enumerator itx
= x.CreateEnumerator();
Token *t = itx.Current();
if(t->Is_Operand()) y.Hiext(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){
<< 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{
DeallocateToken(x);
exit(1);
while(!s.Empty()){
if(s.Top()->Is_LeftP()){
DeallocateToken(x);
exit(1);
}else y.Hiext(s.Pop());
656
// Kiértékelés
try{
Stack<int> v;
BiQueue<Token*>::Enumerator ity
= y.CreateEnumerator();
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()) {
DeallocateToken(x);
exit(1);
}catch(Stack<int>::Exceptions ex){
if(Stack<int>::EMPTYSTACK == ex){
657
cout << „Szintaktikai hiba!” << endl;
DeallocateToken(x);
exit(1);
DeallocateToken(x);
return 0;
BiQueue<Token*>::Enumerator itx
= x.Createenumerator();
delete itx.Current();
658
token.h:
#ifndef TOKEN_H
#define TOKEN_H
#include <string>
#include <sstream>
class Token{
Token*&);
public:
class IllegalElementException{
private:
char ch;
public:
IllegalElementException(char c) : ch®{}
};
virtual ~Token(){}
659
virtual bool Is_Operator() const {return false;}
};
public:
Operand(int v) {val=v;}
protected:
int val;
};
660
class Operator: public Token{
public:
protected:
char op;
};
public:
};
public:
};
public:
};
661
#endif
662
token.cpp:
#include „token.h”
#include <sstream>
#include „stack.hpp”
char ch;
s >> ch;
switch(ch){
s.putback(ch);
int intval;
s >> intval;
t = new Operand(intval);
break;
663
Token::IllegalElementException(ch);
return s;
switch(op){
default: return 3;
switch(op){
default:;
return 0;
stack.hpp:
664
#ifndef STACK_HPP
#define STACK_HPP
#include <iostream>
#include <memory>
class Stack{
public:
Stack();
~Stack();
Stack(const Stack&);
Item Pop();
private:
struct Node{
Item val;
Node *next;
665
Node(const Item &e, Node *n)
: val®, next(n){}
};
Node *head;
};
Stack<Item>::Stack(): head(NULL){}
Stack<Item>::~Stack()
Node *p;
while(head != NULL){
p = head;
head = head->next;
delete p;
666
try{ head = new Node(e,head);}
Item Stack<Item>::Pop()
Item e = head->val;
Node *p = head;
head = head->next;
delete p;
return e;
Item Stack<Item>::Top()const
return head->val;
bool Stack<Item>::Empty()const
667
template <typename Item>
Stack<Item>::Stack(const Stack& s)
else {
try{
}catch(std::bad_alloc o){
throw FULLSTACK;
Node *q = head;
Node *p = s.head->next;
while(p != NULL){
q = q->next;
p = p->next;
668
if(&s == this) return *this;
Node *p;
while(head != NULL){
p = head;
head = head->next;
delete p;
else {
Node *q = head;
Node *p = s.head->next;
while(p != NULL){
q = q->next;
p = p->next;
return *this;
669
}
#endif
670
biqueue.hpp:
#ifndef BIQUEUE_HPP
#define BIQUEUE_HPP
#include <memory>
class BiQueue{
public:
BiQueue():
first(NULL),last(NULL),enumeratorCount (0){}
BiQueue(const BiQueue&);
~BiQueue();
Item Lopop();
Item Hipop();
private:
struct Node{
Item val;
Node *next;
Node *prev;
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);}
private:
BiQueue *bq;
Node *current;
};
Enumerator CreateEnumerator()
{return Enumerator(this);}
};
BiQueue<Item>::~BiQueue(){
672
Node *p, *q;
q = first;
while( q != NULL){
p = q;
q = q->next;
delete p;
else{
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;
673
BiQueue<Item>& BiQueue<Item>::operator=(
Node *p = first;
while(p != NULL){
Node *q = p->next;
delete p;
p = q;
else{
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;
674
Node *p = new Node(e,first,NULL);
first = p;
Item BiQueue<Item>::Lopop(){
int e = first->val;
Node *p = first;
first = first->next;
delete p;
return e;
last = p;
675
if(NULL == first) first = p;
Item BiQueue<Item>::Hipop(){
int e = last->val;
Node *p = last;
last = last->prev;
delete p;
return e;
#endif
676
37. Feladat: Bináris fa bejárása
Specifikáció
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
678
Item Item
Item Item
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
Implementálás
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
class Action{
public:
};
Node osztály
class Node {
public:
protected:
Item val;
};
683
LinkedNoded osztály
public:
LinkedNode(const Item& v,
private:
LinkedNode *left;
LinkedNode *right;
};
684
Bintree osztály
class BinTree{
public:
BinTree():root(NULL){srand(time(NULL));}
virtual ~BinTree();
685
{Pre (root, todo);}
{Post(root, todo);}
protected:
LinkedNode<Item> *root;
BinTree(const BinTree&);
};
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.
void BinTree<Item>::
if(NULL == r) return;
todo->Exec(r);
Pre(r->left, todo);
Pre(r->right, todo);
void BinTree<Item>::
if(NULL == r) return;
In(r->left, todo);
todo->Exec(r);
In(r->right, todo);
687
void BinTree<Item>::
if(NULL == r) return;
Post(r->left, todo);
Post(r->right, todo);
todo->Exec(r);
public:
};
688
template < typename Item>
BinTree<Item>::~BinTree()
DelAction del;
ost(root, &del);
new LinkedNode<Item>(e,NULL,NULL);
else {
LinkedNode<Item> *r = root;
int d = rand();
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);
Tevékenység osztályok
public:
Maxsearch(){l = false;}
if(node->IsLeaf()){
if(!l){
690
l = true;
max = node->Value();
}else if(node->Value()>max)
max = node->Value();
private:
int max;
bool l;
};
691
class Summation: public Action<int>{
public:
Summation(): s(0){}
private:
int s;
};
public:
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);
Printer<int> print(cout);
t.PreOrder(&print);
t.InOrder(&print);
t.PostOrder(&print);
693
Summation sum;
t.PreOrder(&sum);
Maxsearch ms;
t.PreOrder(&ms);
694
Tesztelés
695
Teljes program
main.cpp:
#include "bintree.hpp"
#include <iostream>
ostream& s;
public:
};
public:
Summation(): s(0){}
private:
int s;
696
};
public:
Maxsearch(){l = false;}
if(node->IsLeaf()){
if(!l){
l = true;
max = node->Value();
}else if(node->Value()>max)
max = node->Value();
private:
int max;
bool l;
};
697
int main()
BinTree<int> t;
int i;
while(cin >> i) {
t.RandomInsert(i);
Printer<int> print(cout);
t.PreOrder(&print);
t.InOrder(&print);
t.PostOrder(&print);
Summation sum;
t.PreOrder(&sum);
698
cout << "Fa elemeinek összege:"
Maxsearch ms;
t.PreOrder(&ms);
return 0;
699
bintree.hpp:
#ifndef BINTREE_H
#define BINTREE_H
#include <cstdlib>
#include <time.h>
class Node {
public:
protected:
Item val;
};
class Action{
public:
700
};
class BinTree{
public:
BinTree():root(NULL){srand(time(NULL));}
virtual ~BinTree();
{Post(root, todo);}
protected:
LinkedNode<Item> *root;
BinTree(const BinTree&);
};
701
template < typename Item>
BinTree<Item>::~BinTree()
DelAction del;
Post(root, &del);
new LinkedNode<Item>(e,NULL,NULL);
else {
LinkedNode<Item> *r = root;
int d = rand();
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);
void BinTree<Item>::
if(NULL == r) return;
todo->Exec(r);
Pre(r->left, todo);
Pre(r->right, todo);
void BinTree<Item>::
if(NULL == r) return;
In(r->left, todo);
todo->Exec(r);
In(r->right, todo);
703
template < typename Item>
void BinTree<Item>::
if(NULL == r) return;
Post(r->left, todo);
Post(r->right, todo);
todo->Exec(r);
#endif
704
C++ kislexikon
private: Osztaly();
void Method() = 0;
};
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();
class O { … T … };
void Fv(… T …) { … 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
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.
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).
709
15-3. ábra. Tömb felsorolójának osztály-sablon
710
15-4. ábra. A programozási tételek ősosztály-sablonja
Do(enor.Current());
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.
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.
713
15-7. ábra. Az általános maximumkeresés osztály-sablonja
714
15-8. ábra. A kiválasztás osztály-sablonja
715
15-9. ábra. A lineáris keresés osztály-sablonja
716
Item Item
Item Item
Item Item
Item
717
template <typename Item>
class Enumerator{
public:
virtual ~Enumerator(){}
};
protected:
std::ifstream f;
Item df;
public:
f.open(str.c_str());
if(typeid(Item) == typeid(char))
718
f.unsetf(std::ios::skipws);
};
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.
class Procedure{
protected:
Enumerator<Item> *enor;
Procedure():enor(NULL){}
{return true;}
WhileCond(enor->Current());
public:
void Run();
{enor = en;}
virtual ~Procedure(){}
720
};
void Procedure<Item>::Run(){
Init();
Do(enor->Current());
721
megtörtént-e máshol a helyfoglalás. A *result értékét a Result()publikus
metódussal kérdezhetjük le.
protected:
ResultType *result;
bool inref;
Summation(){
Summation(ResultType *r){
{ return true; }
public:
};
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.
public:
Counting():Summation<Item,int>(){}
protected:
{++*Summation<Item,int>::result;}
};
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.
class Greater{
public:
};
protected:
bool l;
Item optelem;
724
Value opt;
Compare better;
{return true;}
public:
};
void MaxSearch<Item,Value,Compare>::
if ( !Cond(current) ) return;
if (l){
if (better(val,opt)){
725
}
}else{
protected:
void Init(){}
return !Cond(
Procedure<Item>::enor->Current());
public:
726
Item Elem() const
{return Procedure<Item>::enor->Current();}
};
protected:
bool l;
Item elem;
return (optimist?l:!l)
&& Pocedure<Item>::LoopCond();
public:
727
Item Elem() const { return elem;}
};
728
38. Feladat: Kiválogatás
Megoldási terv
A = ( f : enor(ℤ), cout : ℕ* )
Ef = ( f=f’ )
Uf = ( cout = e )
e f'
e páratlan
Implementálás
729
{ return e%2!=0;}-val írjuk felül, akkor akkor csak a páratlan számok
kerülnek kiírásra.
public:
Selecting(ostream *o):
Summation<int,ostream>(o){}
protected:
void Init(){}
};
Selecting pr(&cout);
SeqInFileEnumerator<int> f("inp.txt");
pr.AddEnumerator(&f);
pr.Run();
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.
Teljes program
#include <iostream>
#include <iomanip>
#include "seqinfileenumerator.hpp"
#include "summation.hpp"
731
public:
Selecting(ostream *o):
Summation<int,ostream>(o){}
protected:
int db;
};
int main()
try{
Selecting pr(&cout);
SeqInFileEnumerator<int> f("inp.txt");
pr.AddEnumerator(&f);
pr.Run();
}catch(SeqInFileEnumerator<int>::Exceptions ex){
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
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
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;
};
return f;
class MyMinSearch:
protected:
735
int Func(const Pair &e) const
{ return e.number; }
};
MyMinSearch pr;
SeqInFileEnumerator<Pair> f("input.txt");
pr.AddEnumerator(&f);
pr.Run();
if (pr.Found()){
Pair p = pr.OptElem();
736
Tesztelés
737
Teljes program
#include <iostream>
#include "seqinfileenumerator.hpp"
#include "maxsearch.hpp"
struct Pair{
int time;
int number;
};
class MyMinSearch:
protected:
{ return e.number; }
};
int main()
738
try{
MyMinSearch pr;
SeqInFileEnumerator<Pair> f("inp.txt");
pr.AddEnumerator(&f);
pr.Run();
if (pr.Found()){
Pair p = pr.OptElem();
<< p.Hour()
}catch(SeqInFileEnumerator<Pair>::Exceptions ex){
return 0;
return f;
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
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
struct Pair{
string name;
int count;
};
public:
{return e.count>=5;}
};
741
MyLinSearch pr;
PairEnumerator t("inp.txt");
pr.AddEnumerator(&t);
pr.Run();
if (pr.Found())
protected:
SeqInFileEnumerator<std::string> *f;
Pair current;
bool end;
public:
try{ f =
new SeqInFileEnumerator<std::string>(str);
}catch(SeqInFileEnumerator<std::string>::
742
Exceptions ex){
exit(1);
~PairEnumerator(){ delete f; }
void Next();
};
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.
public:
protected:
string name;
{ return e == name; }
void First(){}
};
void PairEnumerator::Next(){
current.name = f->Current();
NameCounting pr(current.name);
pr.AddEnumerator(f);
744
pr.Run();
current.count = pr.Result();
Tesztelés
Teljes program
#include "pairenumerator.h"
#include "linsearch.hpp"
745
class MyLinSearch : public LinSearch<Pair>{
public:
{ return e.count>=5; }
};
int main()
MyLinSearch pr;
PairEnumerator t("inp.txt");
pr.AddEnumerator(&t);
pr.Run();
if (pr.Found())
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;
};
protected:
SeqInFileEnumerator<std::string> *f;
Pair current;
bool end;
public:
try{ f =
747
new SeqInFileEnumerator<std::string>(str);
}catch(SeqInFileEnumerator<std::string>::
Exceptions ex){
exit(1);
~PairEnumerator(){ delete f; }
void Next();
};
#endif
748
pairenumarator.cpp:
#include "pairenumerator.h"
#include "counting.hpp"
public:
protected:
string name;
{ return e == name; }
void First(){}
};
void PairEnumerator::Next(){
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
Megoldási terv
751
akt : Szó szó_eleje(f) ha vége akkor
szó_vége(f,akt)
szó_eleje(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
struct Word{
string str;
bool hasW;
};
protected:
{ return e.str.size();}
};
MyMaxSearch pr;
WordEnumerator t("inp.txt");
pr.AddEnumerator(&t);
754
pr.Run();
if (pr.Found())
else
protected:
SeqInFileEnumerator<char> *f;
Word current;
bool end;
public:
catch(
SeqInFileEnumerator<char>::Exceptions ex){
exit(1);
755
}
void First();
void Next();
};
756
void WordEnumerator::Next(){
SearchWInWord pr1;
pr1.AddEnumerator(f);
pr1.Run();
current.str = pr1.Result();
SearchEndOfWord pr2;
pr2.AddEnumerator(f);
pr2.Run();
current.str += pr2.Result();
SearchNextWord pr3;
pr3.AddEnumerator(f);
pr3.Run();
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();
protected:
return Procedure<char>::enor->End() ||
758
(e != ' ' && e != '\t'
void First(){}
};
class SearchWInWord
protected:
void First(){}
};
class SearchEndOfWord
protected:
759
return e != ' ' && e != '\t'
void First(){}
};
760
Tesztelés
Érvénytelen adatok:
1. Nem létező állománynév.
761
Teljes program
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;
};
protected:
SeqInFileEnumerator<char> *f;
Word current;
bool end;
762
public:
catch(
SeqInFileEnumerator<char>::Exceptions ex){
exit(1);
void First();
void Next();
};
#endif
763
wordenumarator.cpp:
#include "wordenumerator.h"
#include "selection.hpp"
#include "summation.hpp"
protected:
return Procedure<char>::enor->End() ||
void First(){}
};
class SearchWInWord
protected:
764
&& e != '\r' && e != '\n';
void First(){}
};
class SearchEndOfWord
protected:
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.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"
766
class MyMaxSearch : public MaxSearch<Word,int> {
protected:
{ return e.str.size();}
};
int main()
MyMaxSearch pr;
WordEnumerator t("inp.txt");
pr.AddEnumerator(&t);
pr.Run();
if (pr.Found())
else
return 0;
767
Megoldási terv
A = ( t : enor(ℤ ), z : seq(ℤ) )
Ef = ( t=t’ )
Uf = ( z = t’ )
y : enor(ℤ) y.First()
akt : ℤ Next()
vége :
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()
Implementálás
protected:
769
public:
Union(ofstream *f):Summation<int,ofstream>(f){}
};
protected:
SeqInFileEnumerator<int> *x;
SeqInFileEnumerator<int> *y;
int current;
bool end;
public:
770
try{
x = new SeqInFileEnumerator<int>(str1);
y = new SeqInFileEnumerator<int>(str2);
}catch
(SeqInFileEnumerator<int>::Exceptions ex){
exit(1);
void Next();
};
void CombineEnumerator::Next() {
771
&& x->Current() < y->Current())){
current = x->Current();
x->Next();
current = y->Current();
y->Next();
current = x->Current();
x->Next(); y->Next();
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
Érvénytelen adatok:
1. Nem létező állomány nevek
774
Kiegészítés
void CombineEnumerator::Next(){
x->Next();
Next();
y->Next();
Next();
current = x->Current();
x->Next(); y->Next();
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"
protected:
SeqInFileEnumerator<int> *x;
SeqInFileEnumerator<int> *y;
int current;
bool end;
public:
try{
x = new SeqInFileEnumerator<int>(str1);
y = new SeqInFileEnumerator<int>(str2);
777
}catch
(SeqInFileEnumerator<int>::Exceptions ex){
exit(1);
void Next();
};
#endif
778
combineenumerator.cpp:
#include "combineenumerator.h"
void CombineEnumerator::Next() {
current = x->Current();
x->Next();
current = y->Current();
y->Next();
current = x->Current();
x->Next(); y->Next();
main.cpp:
#include <fstream>
779
#include <cstdlib>
#include "combineenumerator.h"
#include "summation.hpp"
protected:
public:
Union(ofstream *f):Summation<int,ofstream>(f){}
};
int main()
CombineEnumerator t("inp1.txt","inp2.txt");
Union pr(&u);
pr.AddEnumerator(&t);
pr.Run();
return 0;
780
IRODALOM JEGYZÉK
781