4. fejezet - Programozási konstrukciók


Ebben a fejezetben megmutatom, hogyan lehet az eddig tanultak segítségével összetettebb programokat írni. Háromféle vezérlési szerkezetet említek meg, a szekvenciát, az elágazást és a ciklust. Említést teszek még a blokkokról is. Nem titkolt cél, hogy a bevproggal küszködő gólyák valamihez kapcsolni tudják azt a sok absztrakt akármit.



Ez a legegyszerűbb konstrukció, ami arról szól, hogy két vagy több utasítást közvetlenül egymás után végrehajtunk. A példában legyen az a feladat, hogy összeadunk két számot, és az összeg legyen hét. Nyilván ezt a feladatot lebonthatjuk három részfeladatra, mondjuk az egyik változó értéke legyen kettő (F1), a másiké legyen öt (F2), majd adjuk össze ezeket (F3). A feladatot megoldó szekvencia tehát három programból fog állni (S1, S2, S3):

CODE
int a, b, c; a = 2; // S1 b = 5; // S2 c = a + b; // S3

Ez egy három tagú szekvencia és rögtön látszik, hogy például S1 és S2 felcserélhető, de S2 és S3 nem. A megoldás során dekompozíciót alkalmaztam, azaz a feladatot kisebb feladatokra bontottam, amiket már könnyen meg lehet csinálni. Ennek párja a kompozíció, amikor már meglévő programegységekböl építjük fel a megoldást (megj.: nem ez a legmegfelelőbb példa rá, ld. inkább 5. fejezet - Alprogramok).


Ez már sokkal érdekesebb konstrukció, sőt már találkoztunk is vele az előző cikkben. Azt csinálja, hogy egy bizonyos feltétel teljesülésétől függően hajt vagy nem hajt végre egy programot. A feltétel akkor lesz igaz, ha a benne szereplő kifejezés értéke nem nulla, egyébként hamis lesz. Ha logikai kifejezés áll a feltétel helyén, akkor annak true vagy false az értéke, ahol az utóbbi nulla, az előbbi meg nem nulla. A logikai értékek típusa a bool, ez a C++ -ban lett bevezetve.

CODE
int a; std::cout << "Irj be egy szamot: "; std::cin >> a; if( a < 0 ) std::cout << "Ez a szam negativ\n";

Lehetőség van megadni a másik ágat is, vagyis azt hogy mi történjen akkor, amikor nem teljesül a feltétel. Ezt az else kulcsszóval tehetjük meg.

CODE
int a; std::cout << "Irj be egy szamot: "; std::cin >> a; if( a < 0 ) std::cout << "Ez a szam negativ\n"; else std::cout << "Ez a szam pozitiv\n";

Itt fel is merül egy probléma. Tekintsük az alábbi kódot:

CODE
if( a < 0 ) if( b > 5 ) // valami else // akkor ez most melyik if-hez tartozik?

Ezt úgy hívják, hogy csellengő else. C++ -ban mindig a legbelső if -hez tartozik az else, ha a külsőhöz akarod kapcsolni, akkor ki kell tenni a kapcsos zárójeleket. Vannak olyan nyelvek is, például a Python, amelyekben a behúzás mértéke határozza meg, hogy melyikhez tartozik (vagyis a példában a külsőhöz tartozna). Ha egy ágban nem akarsz semmit sem csinálni, akkor írhatsz egy sima ; -t. A kapcsos zárójeleket kitéve így néz ki a program:

CODE
if( a < 0 ) { if( b > 5 ) ; } else ;

Az elágazásoknak van egy másik fajtája is, amit esetkiválasztásos elágazásnak hívnak. A vezérlés arra az ágra fog ugrani, amelyik igazra értékelődött ki. Vigyázni kell azonban, ugyanis ha nem írsz az ág végére break -et, akkor a vezérlés tovább fog folyni a következő esetre (és ez egy fontos különbség a struktrogramoknál használt elágazáshoz képest). A szelektor típusa integral típusú kell legyen (pl. int, char, long).

CODE
int a; std::cout << "Irj be egy szamot: "; std::cin >> a; switch( a ) { case 3: std::cout << "3-at irtal be\n"; break; case 5: std::cout << "5-ot irtal be\n"; default: std::cout << "Nemtom mit irtal be\n"; }

Ha hármat írok be neki, akkor minden rendben, ha viszont ötöt, akkor azt is kiírja, hogy nem tudja mit írtam be. Ez azért történt mert az ötnél nem írtam break -et és átfolyt a vezérlés. Ennek persze lehet haszna is. Az is látható, hogy a nem lefedett esetekre lehet használni a default utasítást, ez akkor fut le, ha az összes többi hamisra értékelődött ki. C++ -ban nem kötelező minden esetet lefedni, de például Ada-ban igen.


Ugye mindenki álmából felriasztva keni-vágja a ciklus definícióját, levezetési szabályát és annak megfordítását? Miazhogy neeeem? Nyomás vissza bevprogot tanulni...

Az eddigiekben a programok egyszer hajtódtak végre, aztán kiléptek. Elegánsabb lenne, ha a felhasználó mondhatná meg, hogy mikor akar kilépni. Erre (is) szolgál a ciklus nevű konstrukció. Ennek több fajtája is van, először kezdjük a legegyszerűbbel, ami for ciklus névre hallgat. Mondjuk írjuk ki a számokat egytől tízig:

CODE
for( int i = 1; i < 11; ++i ) std::cout << i << " "; // ciklusmag std::cout << "\n";

A for ciklus három részből áll, egy inicializáló lépésből, egy feltételből, és egy rákövetkezésből. Az inicializáló lépés egyszer fut le, itt állhat deklaráció is, azonban az itt deklarált változók a ciklusra nézve lokálisak lesznek (ld. következő alpont), és a végén felszabadulnak (amennyiben automaikusan lettek létrehozva, ld. későbbi cikk). Ha a feltétel teljesül, akkor végrehajtjuk a ciklusmagot, majd a végén a rákövetkezést, és ismét ellenőrizzük a feltételt. Így megy ez addig amíg a feltétel hamis nem lesz. Rögtön eszébe jut mindenkinek, hogy baromi könnyű végtelen ciklust csinálni, ami sosem áll le.

Ha most bevprogos fejjel gondolkozunk, akkor a fenti ciklus invariánsa mondjuk az, hogy i > 0 (totál értelmetlen), a terminálási feltétele pedig az, hogy i >= 11.

Egy másik fajta ciklus amit elöltesztelő vagy while ciklusnak hívnak a következőképpen fest:

CODE
int a, b; bool fusson = true; char ch; while( fusson ) { system("cls"); // linux alatt "clear" std::cout << "Irj be ket szamot: "; std::cin >> a >> b; std::cout << "A ket szam osszege: " << a + b << "\n"; std::cout << "Akarod folytatni? y - igen, n - nem: "; std::cin >> ch; fusson = (ch == 'y'); }

Ez a ciklus is hasonlóan addig fog futni, amíg a feltétele igaz. Azért híjuk elöltesztelőnek, mert a feltételt a ciklusmag előtt értékeli ki. Felmerül a gyanú, hogy akkor van hátultesztelő ciklus is, valóban. Írjuk meg a programot szebben!

CODE
int a, b; char ch; do { system("cls"); // linux alatt "clear" std::cout << "Irj be ket szamot: "; std::cin >> a >> b; std::cout << "A ket szam osszege: " << a + b << "\n"; std::cout << "Akarod folytatni? y - igen, n - nem: "; std::cin >> ch; } while( ch == 'y' );

Megspóroltunk egy változót. Nyilván a feladattól függ, hogy mikor melyik ciklust alkalmazzuk. Ha egy ciklusból szeretnénk idő előtt kilépni, akkor a break utasítást kell használni. Ha szeretnénk visszaugrani a feltételhez, akkor a continue utasítást használhatjuk. Ezekkel megvalósítható a középen tesztelő ciklus is. Elvetelmülteknek mondom, hogy hátulgombolós ciklus nincs, csak hátulgombolós programozó.

Írjunk egy olyan programot, ami kiírja 0 és 10 között a páros számokat és használjuk mind a két említett utasítást:

CODE
int i = -1; while( true ) { ++i; if( i > 10 ) break; if( i % 2 == 1 ) continue; std::cout << i << " "; } std::cout << "\n";

Kicsit erőltetett és nem túl olvasható példa, de legalább látható hogyan kell használni ezeket. Ha túllépte a 10-et, akkor kilép a ciklusból, egyébként ha a szám páratlan, akkor visszaugrik a ciklus elejére, ha meg páros akkor kiírja.


Ez nem vezérlési szerkezet (nem is megengedett konstrukció?), nem is tanultátok bevprogból, mert minek (egyébként leírható), de mégis egy fontos nyelvi elem. A fentiekben is voltak blokkok, ilyenkor egy új láthatósági szint jön létre, tehát az új blokkon belül újra lehet deklarálni már létező változókat, de azok elfedik az addigiakat.

CODE
int i = 2; int main() { int i = 6; { int i = 3; std::cout << i << "\n"; std::cout << ::i << "\n"; { std::cout << ::i << "\n"; } } // output: 3 2 2 // ... }

Egy blokkon belül deklarált változókat a blokkra nézve lokálisnak, az azon kívül deklaráltakat globálisnak hívjuk. Egy blokkon belül deklarált változók csak a blokkon belül láthatóak, a blokkból való kilépéskor már nem érhetőek el. Ez persze nem mindig jelenti azt, hogy meg is semmisülnek, erről majd lesz egy külön fejezet. Nyilván egy blokk lokális változói a beágyazott blokkokra nézve globálisak lesznek, tehát a beágyazott blokk látja a tartalmazó blokk lokális változóit. Ha ennek ismeretében nézzük meg az elágazást és a ciklust, akkor ugyanezt mondhatjuk el: egy elágazáson vagy cikluson belül deklarált változó lokális lesz arra nézve, a lefutása után már nem érhető el.

A legkülső szinten (globális névtér) deklarált változókat elérhetjük a :: (scope) operátorral, de például közbülső szinteket nem tudjuk elérni. Az olyan nyelveket amikben van ilyen blokk-szerű nyelvi elem, blokkstrukturáltnak hívjuk. Például Pascalban a begin ... end is ilyen.


Van egy érdekes, három operandusú operátor, amivel ott is lehet elágazásokat írni, ahol egyébként nem. Például deklarációban. Annyi megszorítás van, hogy a két ágnak konvertálhatónak kell lenni egymásra. Ha két ilyet egymásba ágyaztok, akkor érdemes rendesen bezárójelezni.
Szokták if-then-else operátornak is hívni, mivel a then-nek megfelel a ?, az else-nek pedig a : Némely fordító megengedi, hogy az egyik ágat elhagyd, de a szabvány nem, ezért inkább írd ki mindkét ágat.

CODE
int i; std::cin >> i; int j = (i < 0 ? 1 : (i > 5 ? 3 : 2));

Itt rögtön egymásba is ágyaztam két ilyen kifejezést; logikailag megfelel a következőnek (höfö átirni a C++ nyelvére):
Ha i < 0, akkor j = 1, egyébként ha i > 5, akkor j = 3, egyébként j = 2.

A fejezet végén még írok egy két szám legnagyobb közös osztóját (lnko) kiszámító programot. Ehhez gondoljuk végig mit is jelent ez. Azt jelenti, hogy m-szer is és n-szer is összeadom ugyanazt a számot (az lnko-t), és két különböző számot kapok.
2 + 2 + 2      = 6
2 + 2 + 2 + 2  = 8
Most nézzük meg visszafelé mit jelent! Az ábrán is látható, hogy a nagyobbik számban legalább annyiszor megvan az lnko mint a kisebbikben. Tehát ha a nagyobbik számból kivonom a kisebbiket, akkor az így kapott számoknak még mindig ugyanaz az lnko-ja!
2 + 2 + 2      = 6
2              = 2
Máris adódik, hogy mit kell csinálni: addig vonogatjuk ki a nagyobbikból a kisebbiket amíg egyenlő számokat nem kapunk. Az lesz az lnko. Már csak le kéne programozni. Ciklus és elágazás mindenképpen kelleni fog, nézzük is meg:

CODE
int main() { int a, b; std::cout << "Irj be ket szamot: "; std::cin > a > b; while( a != b ) { if( a > b ) a -= b; else b -= a; } std::cout << "\nA legnagyobb kozos oszto: " << a << "\n"; return 0; }

Van ezzel néhány probléma, nevezetesen, hogy negatív számokra nem jó eredményt ad, illetve ha az egyik szám nulla, akkor szintén nem jó. Mi lehet a baj? Nyilván az, hogy negatív számoknál nem a nagyobból, hanem a kisebből kell kivonogatni. A nulla esetében pedig nem is kell számolni, hiszen az lnko a másik szám lesz. Az első problémára megmondom a megoldást. A feltétel nézzen ki a következőképpen:

CODE
// ... if( a > 0 ? a > b : a < b ) a -= b; else b -= a;

A nullás esetnél a ciklus előtt érdemes ellenőrizni, hogy valamelyik nulla-e. Ez legyen höfö.


Höfö:
  • Írd meg a legkisebb közös többszörös kiszámítását (de ne az lnko-val való kapcsolata alapján számold ki)!

Irodalomjegyzék

http://en.cppreference.com/w/cpp
http://www.cplusplus.com/
http://www.parashift.com/c++-faq-lite/ (ezt különösen ajánlott elolvasni)
http://aszt.inf.elte.hu/~gsd/halado_cpp/ (ezt is)

back to homepage

Valid HTML 4.01 Transitional Valid CSS!