Ciklusok III.
A while-ciklus
Egy programozási feladatot általában többféleképpen is meg lehet oldani az adott programnyelvvel (jelen esetben a PHP-vel). Sok esetben egy adott művelet elvégzésére is többféle mód van. Például, ha az $n változót szeretnénk eggyel növelni, már legalább 3-féleképp meg tudjuk tenni: $n = $n + 1; $n += 1; ++$n;. Talán nem olyan meglepő, hogy ciklust is többféleképpen tudunk írni, pontosabban többféle típus létezik belőle. Az adott problémától függően az egyik típus a kód áttekinthetőségét javíthatja, a másik működésben tér el kissé a többitől, így mindig az adott feladathoz legjobban illeszkedő, vagy legegyszerűbben megírható ciklust választhatjuk ki a probléma megoldására.
Az eddig megismert while-ciklus szerkezete általánosan az alábbi módon néz ki:
while (ciklusfeltétel){
utasítások
}
Természetesen ezzel elég jól elboldogulunk (legalábbis remélem), így felmerülhet a kérdés, miért van egyáltalán szükség másfajta ciklusra? Ha visszanézed az eddig, a tanfolyamon előfordult ciklusokat, szinte mindegyikben szerepelt egy egész számot tartalmazó változó, ami az alábbi módon viselkedett:
$n = 0;
while ($n < 100){
utasítások
++$n;
}
Itt az $n változóról van szó. A fenti ciklus csak egy példa, de sokszor az a feladat, hogy egy adott műveletet meghatározott számú alkalommal meg kell ismételnünk. Ezt a fent látható módon szoktuk csinálni, vagyis egy változónak adunk kezdeti értéket még a ciklus előtt, majd ennek az értékét vizsgáljuk a feltételben, és a ciklusmagban (többnyire a végén) változtatjuk. Ezzel a három dologgal (kezdeti értékadás, feltétel, változtatás) tudjuk vezérelni a ciklust, vagyis azt beállítani, hogy a ciklusmag hányszor ismétlődjön. A fenti példában 100-szor fog lefutni, de a három vezérlő rész közül bármelyiket megváltoztatva ezt módosíthatjuk. Az ilyen változót ciklusváltozónak szokták hívni, ez mindig valamilyen egész szám, és ez végzi az említett módon a ciklus vezérlését.
Mivel a legtöbb ciklus (de nem mindegyik!) hasonlóan néz ki, felmerülhet az igény arra, hogy az említett három vezérlő rész egy helyen legyen, ne pedig így szanaszét, mint a while-ciklus esetén. Ez például abból a szempontból jó, hogy ránézésre meg tudnánk mondani, hányszor fog lefutni a ciklusmag, és a ciklusmag műveleteit el tudnánk választani a ciklus vezérlésétől. Ezt az óhajt teljesíti a for-ciklus, amit most fogunk megismerni.
A for-ciklus
Ez a ciklus abban különbözik a while-ciklustól, hogy egyrészt a for kulcsszóval hozzuk létre, és a szerkezete is kicsit más. Működését tekintve viszont nincs semmi különbség. Általános esetben az alábbi módon néz ki:
for (kezdeti értékadás; feltétel; változtatás){
utasítások
}
Tulajdonképpen az történik, hogy a kerek zárójelek közé nem csak a feltételt írjuk be, hanem a fentebb említett három vezérlő részt, pontosvesszőkkel elválasztva. Például a fenti while-ciklus átalakítva for-ciklussá az alábbi módon néz ki:
for ($n = 0; $n < 100; ++$n){
utasítások
}
Itt az utasítások ugyanaz marad, mint a while-ciklus esetén. Még a ciklusok megismerésének elején említettem egy példát, hogy magyar nyelven hogy néz ki egy while-ciklus (a fenti példát használva):
$n = 0; ismételd az alábbi műveletsort addig, amíg $n < 100: utasítások ++$n; vége
A for-ciklus esetén így lehetne magyarra fordítani a működési elvét:
ismételd meg az alábbi műveletsort 100-szor: utasítások vége
Egy for-ciklus részletesen az alábbi módon működik. A kerek zárójeleken belüli első rész (kezdeti értékadás) a ciklusba való belépés előtt, egyszer hajtódik végre. A második rész a ciklusfeltétel, ennek ugyanaz a szerepe, mint a while-ciklus esetén, ha ez igaz logikai értékű, akkor lefut a ciklusmag. A harmadik rész (változtatás) pedig a ciklusmag minden lefutása után kerül végrehajtásra, a következő feltételvizsgálat előtt. Minden for-ciklus átírható egy vele egyenértékű while-ciklusra, és fordítva, mivel működésben nincs köztük semmi különbség. Az alábbi két ciklus egyenértékű:
utasítás1;
while (feltétel){
utasítás2;
utasítás3;
}
for (utasítás1; feltétel; utasítás3){
utasítás2;
}
Az utasítás2 helyén persze egy tetszőlegesen hosszú utasítássorozat is állhat. A többi utasítás is tetszőleges lehet, bár a for-ciklus kerek zárójelek közti részeit általában a ciklus vezérlésére szokás használni.
Előfordulhat, hogy olyan for-ciklust akarunk írni, amelyben valamelyik vezérlő rész hiányzik. Ekkor azt a részt üresen hagyhatjuk, viszont a pontosvesszőket ekkor is oda kell írnunk! Például a legegyszerűbb végtelen ciklus for-ciklus alakban:
for (;true;){
ciklusmag
}
Látható, hogy a pontosvesszők ott maradtak, csak a kezdeti értékadó és változtató rész üres. Ha több utasítást akarunk elhelyezni valamelyik vezérlő részben (a feltételben csak egy kifejezés szerepelhet), akkor azokat vesszővel kell elválasztani egymástól. A pontosvesszőt nem használhatjuk, mivel az itt a három vezérlő rész elválasztására szolgál. Például előfordulhat (általában beágyazott ciklusoknál), hogy két változó vezérli a ciklust:
for ($i = 0, $j = 100; $i < 70 && $j > 35; ++$i, --$j){
utasítások
}
Ebben az esetben a ciklusmag addig fut, amíg a ciklusváltozók bizonyos kritikus értékeket át nem lépnek. A vesszővel elválasztott utasítások értelemszerűen balról jobbra hajtódnak végre, bár ez a fenti példában lényegtelen. A fenti példával egyenértékű while-ciklus így néz ki:
$i = 0;
$j = 100;
while ($i < 70 && $j > 35){
utasítások
++$i;
--$j;
}
Mivel az első és harmadik vezérlő részben bármilyen utasítás szerepelhet, és a számuk sincs korlátozva, ezért az alábbi ciklus nyelvtanilag helyes:
$n = 0;
for (; $n < 10; print ", ", ++$n){
print $n;
}
Ez azt jelenti, hogy a ciklusmag lefutása ($n értékének kiírása) után még kiíródik egy vessző, és növekszik $n értéke. Ez azonban egy tökéletes példa a for-ciklus helytelen használatára. Mivel a for-ciklus használatának a célja, hogy a vezérlő utasítások egy helyre gyűjtésével áttekinthetőbbé tegye a ciklust, ezért ha lehet, használjuk erre, és ne írjunk bele olyan utasításokat, melyeknek semmi köze a ciklus vezérléséhez.
A do-ciklus
Megismerünk még egy fajta ciklust, ami a while-ciklushoz formailag jobban hasonlít, viszont ez működésében tér el tőle. A while- és a for-ciklus úgynevezett elöltesztelő ciklusok, ami azt jelenti, hogy a ciklusmag lefutása előtt vizsgálódik meg a feltétel, és a mag csak akkor fut le, ha a feltétel teljesül. A do utasítással létrehozott ciklus ezzel szemben hátultesztelő, mivel a ciklusmag végrehajtása után vizsgálja csak meg a feltétel teljesülését. Ez a formáján is látszik:
do {
utasítások
} while (ciklusfeltétel);
Itt láthatóan az történt, hogy a ciklusfeltételt a while kulcsszóval együtt bevágtuk a ciklusmag mögé. Az egyetlen extra, hogy ilyenkor a ciklusmagot határoló kapcsos zárójelek elé kell írnunk a do szót, valamint a feltételt lezáró kerek zárójel után kell pontosvessző. A működési elve hasonló a while-ciklushoz, csak az a különbség, hogy először lefut a ciklusmag, majd utána vizsgálódik meg a feltétel. Ha a feltétel igaz, a ciklusmag megint lefut, ha hamis, akkor a feltétel utáni (vagyis a ciklus utáni) első utasításra ugrik. Persze a while-ciklusnál ugyanez történik, mivel ha véget ért a ciklusmag, akkor utána mindig megvizsgálódik a feltétel. Akkor meg mi a különbség? Az egyetlen különbség a ciklusba történő első belépéskor van. Ugyanis mikor legelőször akar a program belépni a ciklusba, akkor anélkül elkezdi végrehajtani a ciklusmagot, hogy a feltételt megvizsgálná. Ez azt jelenti, hogy a do-ciklus magja legalább egyszer biztosan lefut, függetlenül a feltételtől. A do-ciklus csak akkor fog a vele egyenértékű while-ciklustól eltérően viselkedni, ha a feltétel már a legelső alkalommal sem teljesül. Minden más esetben azonosan működik a két ciklus:
utasítás1;
while (feltétel){
utasítás2;
utasítás3;
}
utasítás1;
do {
utasítás2;
utasítás3;
} while (feltétel);
Látható, hogy a ciklusvezérlést a do-ciklus esetén a while-ciklushoz hasonló módon tudjuk megoldani, vagyis itt sincsenek egy helyre összegyűjtve, mint a for-ciklus esetén. Példa:
$n = 1;
do {
print $n.", ";
++$n;
} while ($n < 10);
print $n.".";
Ez az alábbi kimenetet eredményezi:
1, 2, 3, 4, 5, 6, 7, 8, 9, 10.
Ha a ciklusfeltétel például $n < 0 lenne, akkor már a legelső esetben sem teljesül a feltétel. Ekkor az alábbi kimenet jön létre:
1, 2.
Látható, hogy a ciklusmag egyszer lefutott. Egy ezzel "egyenértékű" while-ciklus csak az 1-es számot írná ki, utána a pontot, mivel az értékadás után rögtön a ciklus utáni utasítás kerülne végrehajtásra.
Összegezve az eddigieket, általában javasolható, hogy ha van ciklusváltozó, és szeretnénk azt elválasztani a ciklusmagtól, akkor for-ciklust érdemes használni, más esetekben egyszerűbb a while-ciklus használata. A do-ciklust viszonylag ritkábban használják (én életemben egyszer használtam), nyilván akkor érdemes használni, ha azt szeretnénk, hogy a ciklusmag legalább egyszer biztosan lefusson.
Házi feladat
1.) Mit csinál ez a ciklus?
$alap = 3;
for ($i = 0, $j = $alap; $i < 10; ++$i, $j *= $alap){
print $j.", ";
}
Ha a futtatása nélkül rájössz, akkor biztosan megértetted a for-ciklus működését. Egyébként ilyen program írása nem éppen követendő példa!
A ciklus azért nem túl megnyerő, mert a $j változónak semmi köze a ciklus vezérléséhez, így a ciklusmagban lenne a helye. Ennek ellenére a ciklus átírása nélkül is rá lehet jönni hogy mit csinál. Az $i ciklusváltozót megnézve látszik, hogy 10-szer fut le, és minden esetben kiírja $j aktuális értékét. $j kezdetben 3, majd mindig megszorzódik 3-mal. Ez azt jelenti, hogy a 3-as szám (pontosabban az $alap változó) első 10 hatványát fogja kilistázni.
2.) Alakítsuk függvénnyé a 7. leckében bemutatott "háromszögrajzoló" programot úgy, hogy a függvénynek paraméterként a háromszög magasságát adjuk át (feltételezzük, hogy pozitív egész szám). Csináljuk meg egyszer úgy, hogy csak for-ciklust használunk, másodszor pedig úgy, hogy csak do-ciklust. Van-e olyan eset, amikor a do-ciklussal írt változat másképp fog viselkedni, mint az eredeti while-ciklusos?
Az eredeti program (az egyszerűség kedvéért a talp nélküli változatot nézzük):
// háromszög talp nélkül:
$magassag = 6;
print '<font face="Courier New" size="2">';
$sor = 1;
while ($sor < $magassag){
$a = 1;
while ($a <= ($magassag - $sor)){
print " ";
++$a;
}
print "/";
$b = 1;
while ($b <= (2 * $sor - 2)){
print " ";
++$b;
}
print "\\<br />";
++$sor;
}
print '</font>';
Egyszerűen függvénnyé alakíthatjuk úgy, hogy a $magassag változót paraméterként adjuk meg. A while-ciklusok helyett pedig for-ciklusokat írva:
function draw_triangle_for($magassag){
print '<font face="Courier New" size="2">';
for ($sor = 1; $sor < $magassag; ++$sor){
for ($a = 1; $a <= ($magassag - $sor); ++$a){
print " ";
}
print "/";
for ($b = 1; $b <= (2 * $sor - 2); ++$b){
print " ";
}
print "\\<br />";
}
print '</font>';
}
Hogy a lényegre koncentráljunk, most nem alakítottam át úgy, hogy visszatérési értékként adja át a háromszöget, magmaradt a kiíratás. A for-ciklusok vezérlő részébe csak a ciklusváltozókat tettem be, ez a külső ciklus esetén a $sor változó, a belsőknél pedig az $a és a $b.
Most nézzük, hogy néz ki az egész do-ciklusokkal:
function draw_triangle_do($magassag){
print '<font face="Courier New" size="2">';
$sor = 1;
do {
$a = 1;
do {
print " ";
++$a;
} while ($a <= ($magassag - $sor));
print "/";
$b = 1;
do {
print " ";
++$b;
} while ($b <= (2 * $sor - 2));
print "\\<br />";
++$sor;
} while ($sor < $magassag);
print '</font>';
}
A while-ciklust átírni do-ciklussá nem nagy kunszt, most nézzük meg, van-e működésbeli különbség köztük. Legutóbb láttuk, hogy különbség akkor van, ha a ciklusfeltétel már a belépéskor sem teljesül. Ahelyett, hogy elkezdenénk okoskodni, érdemes kipróbálni a programot! Azt tapasztaljuk, hogy a do-ciklusos változat a háromszög első sorát a $magassag értékétől függetlenül, sosem rajzolja ki normálisan. A háromszög két szára közé rak egy szóközt, pedig nem kellene. Ezért a szóközért a második belső ciklus a felelős, és látható, hogy ennek a feltétele az első sor kiírásakor ($b <= (2 * $sor - 2) ami az első sor esetén 1 <= 0) még nem teljesül, vagyis a while-ciklusos változat egyszer sem fut le. A do-ciklus viszont egyszer lefut, így a szóközt mindig odaírja.







