Ciklusok II.
Végtelen ciklusok
Ha a múltkori példaprogramokat próbálgattad vagy a fenti feladatokat csináltad, előfordulhatott, hogy a böngésző kb. fél percnyi látszólagos semmittevés után kiírt egy hibaüzenetet, vagy esetleg teljesen lefagyott. Látszik, hogy ciklusokat nem lehet ész nélkül írkálni! Hogy megértsük miről is van szó, nézzük meg például a következő programot:
// végtelen ciklus (ártalmatlan változat):
$x = 20;
while ($x > 1){
++$x;
}
Mi a baj ezzel? A fenti példában az történik, hogy az $x változónak 20-at adunk kezdőértékül, majd elindítunk egy ciklust. A ciklusfeltételből látszik, hogy a ciklus csak akkor áll le, ha $x értéke 1 alá csökken (vagy azzal egyenlő). A ciklus azonban semmi mást nem csinál, mint minden egyes lefutásnál eszetlen módon növeli $x értékét, ami így sosem lesz 1-nél kisebb. Mi van ilyenkor? Ilyenkor az a helyzet, hogy a program belép a ciklusba, és mivel a feltétel mindig igaz lesz, ezért ki se lép onnan, így viszont a program futása sosem ér véget. A ciklusból ugyanis a program csak akkor lép ki, ha a feltétel hamissá válik, vagy a cikluson belül erre utasítást adunk neki, a fenti példában viszont ilyen nem történik. Az ilyen ciklust nevezik végtelen ciklusnak!
Javaslom a fenti példaprogram kipróbálását, mivel semmi kárt nem csinál. A szerver 30 másodpercig futtatja a programot, majd ha még mindig nem futott le (itt nem fog az említett okok miatt), akkor egy ehhez hasonló hibaüzenetet ír ki:
Fatal error: Maximum execution time of 30 seconds exceeded in D:\wamp\www\pelda2.php on line 5
Ez azt jelenti, hogy a program (jelen esetben a pelda2.php nevű fájl) a maximálisan megengedett 30 másodpercig futott, és az 5-ödik sorban tartott, amikor a szerver megszakította a futását. Nem biztos hogy nálad is 30 másodperc a limit, mivel ez átállítható a szerver egyik beállítóállományában. Az ilyen hibaüzenet a legtöbb esetben arra utal, hogy a program szóban forgó sora (ebben az esetben az 5-ödik) egy végtelen ciklusban helyezkedik el.
A fenti példa még a szerencsésebb helyzet, mivel kiderül belőle hogy nagyjából hol van a végtelen ciklus. Nézzük meg viszont a fenti példa módosított változatát:
// végtelen ciklus (böngészőgyilkos változat):
$x = 1;
while ($x != 8){
print $x;
$x = $x + 2;
}
Ennek a programnak a kipróbálását csak az erősebb idegzetűeknek ajánlom! :D Ez is végtelen ciklus, mivel a változó értéke eredetileg 1, és utána kettesével növekszik, tehát mindig páratlan szám lesz. Fel fogja venni a 7-es, majd a 9-es értéket, a 8-asat viszont kihagyja, így a feltétel (nem egyenlő 8-cal) mindig igaz lesz! Nálam például a futtatásnál az történt, hogy elkezdte kiírni a számokat, majd a böngésző "lefagyott". Nyilván az a probléma, hogy ki is akarjuk íratni azt a végtelen sok számot. Mire a 30 másodperc letelik, addigra a szám már valamilyen trilliós nagyságrendű lehet (és akkor még lehet hogy túl kicsit mondtam), ez viszont azt jelenti, hogy a böngészőnek legalább ennyi karaktert kéne kiírnia a képernyőre. Ezt persze meg is csinálja, csak jó sokáig tart, és amíg ezen dolgozik, addig még véletlenül sem fog reagálni az ablak a kattintgatásainkra és olyan mintha lefagyott volna. Persze ha a tálcán a gombjára jobb gombbal kattintasz és kiválasztod a "Bezárás"-t, akkor előbb-utóbb bezáródik.
Az egészből az a tanulság, hogy érdemes észnél lenni, amikor a ciklus feltételét megírjuk. A ciklus feltétele és magja lehet a fenti példáknál sokkal bonyolultabb, és akkor már egyáltalán nem biztos hogy első látásra meg lehet mondani, hogy végtelen-e vagy sem.
Beágyazott ciklusok
Múltkor megtárgyaltuk hogy miket írhatunk a ciklusfeltételbe, a ciklusmagról azonban nem sok szó esett. Az eddigiek alapján persze látszik, hogy ide utasításokat írhatunk, tetszőlegesen sokat. Új változókat is létrehozhatunk itt (mivel az is utasítás), és az itt létrehozott változókat a ciklusból való kilépés után is használhatjuk (vagyis megmarad az értékük)! Ezt csak azért említettem meg, mert a legtöbb programozási nyelvben nem így van.
Sok esetben szükség lehet arra, hogy a ciklusmagban egy másik ciklust hozhassunk létre. Ezt meg lehet csinálni, az ilyet hívják beágyazott ciklusnak. A beágyazást tetszőleges mélységig csinálhatjuk, tehát a cikluson belüli ciklusban is létrehozhatunk ciklust, és természetesen egy ciklusba több különálló ciklust is beágyazhatunk egymás után. Nézzünk egy egyszerű példát, ahol beágyazott ciklust célszerű használni:
Írassuk ki 1-től 15-ig a számokat úgy, hogy egy sorba csak 5 számot írunk! Vagyis a kimenetnek ilyennek kell lennie:
1, 2, 3, 4, 5,
6, 7, 8, 9, 10,
11, 12, 13, 14, 15,
Először oldjuk meg a problémát egyetlen ciklussal a 2. házi feladathoz hasonló módon:
$x = 1;
while ($x <= 15){
print $x.", ";
++$x;
print $x.", ";
++$x;
print $x.", ";
++$x;
print $x.", ";
++$x;
print $x.", ";
++$x;
print "<br />";
}
A programot a 2. házi A változatához hasonló módon írtam. A B változat rövidebb lett volna, itt viszont jobban látszik, hogy hogyan lehet ciklussá alakítani az ismétléseket. Először nézzük csak a fenti ciklus magját, az ebben lévő ismétléseket rakjuk ciklusba:
// a fenti ciklus magja:
$y = 1;
while ($y <= 5){
print $x.", ";
++$x;
++$y;
}
print "<br />";
Ennek a ciklusnak a feltételében nyilván egy másik változót kell használnunk, aminek azonban semmi köze a kiírandó számhoz. Ezt a változót $y-nak neveztem. Ez a ciklus végzi a programban egy 5 számból álló sor kiírását. Most helyettesítsük a fenti kóddal a külső ciklus magját:
// a teljes program:
$x = 1;
while ($x <= 15){
$y = 1;
while ($y <= 5){
print $x.", ";
++$x;
++$y;
}
print "<br />";
}
Most nézzük meg részletesen, hogy miként működik a fenti kód! $x lesz az a változó, amelyben a kiírandó számot tároljuk, ez beállítjuk 1-re. Belépünk a főciklusba (külső ciklus), ami nyilván addig fut, amíg $x nem nagyobb 15-nél. Most kreálunk egy új változót ($y), ami kezdetben szintén 1. Lefut a belső ciklus, ami mindig kiírja $x értékét, majd növeli eggyel. Eközben viszont $y értékét is növeli, és amint 5 számot kiírt, véget ér a ciklus, és kiírunk egy sortörést (<br />). Most értünk a főciklus végére, megvizsgálódik a feltétel: $x értéke 6, újra elindul a főciklus. $y értéke szintén 6, ezt megint 1-re állítjuk, $x értéke azonban 6 marad, igy a 6-os számmal kezdődik a következő sor kiírása. Miután ez lefutott, $x értéke már 11, kiírunk ismét egy sortörést, majd jön a főciklus feltételvizsgálata. Mivel ez még mindig igaz, ismét elindul, kiírja a harmadik sort. Mikor újra véget ér a belső ciklus, $x értéke már 16, kiírunk egy sortörést, és jön a feltételvizsgálat. Mivel $x <= 15 már nem teljesül, ezért kilépünk a főciklusból. $y értéke pedig a belső ciklus lefutása miatt 6, ezt a főciklusból való kilépés után ha akarjuk felhasználhatjuk, de ebben a példában nincs rá szükség.
Nézzünk meg egy bonyolultabb (és kicsit furcsább) példát beágyazott ciklus használatára:
Rajzoljunk ki karakterekből egy háromszöget! Használjuk a / (perjel) \ (backslash) és _ (aláhúzás) karaktereket és a "Courier New" betűtípust! Tároljuk a háromszög magasságát egy $magassag nevű változóban! A kimenet legyen ilyen:
/ \
/ \
/ \
/ \
/__________\
Jelen esetben $magassag = 6. Azt kell tudni, hogy a HTML-ben hiába írunk több szóközt egymás után, mindig csak egy jelenik meg. Ha több szóközt akarunk írni, akkor az kódot kell használnunk (egy ilyen kód egy szóközt helyettesít). Most a feladatot a háromszög talpának kihagyásával (legalsó sor) oldjuk meg, a talp hozzárajzolása házi feladat lesz.
Nyilván soronként kéne kiírni ezeket a karaktereket, így kell írni egy ciklust, ami a magjában az egyes sorokat hozza létre. Vagyis a program váza így néz ki:
// háromszög talp nélkül (váz):
$magassag = 6;
print '<font face="Courier New" size="2">';
$sor = 1;
while ($sor < $magassag){
// egy sor kiírása...
++$sor;
}
print '</font>';
Itt a $sor változó fogja megadni, hogy hányadik sorban tartunk, ezt a ciklus belsejében felhasználhatjuk. Beraktam a <font> tegeket a feladat kérésének megfelelően, és a $magassag változó határozza meg a magasságot. A $sor változóval azért csak ($magassag - 1)-ig megyünk (lásd a ciklusfeltételt), mert a talpát nem akarjuk kirajzolni, azt a ciklus után kell külön megtennünk! Most vizsgáljuk meg, hogyan írhatunk ki egy sort!
Látható, hogy a sorok hasonlítanak egymásra, valamilyen szabályszerűség van a szóközök száma, az egyes karakterek elhelyezkedése között (ez teszi lehetővé, hogy ciklust írjunk). Látható, hogy minden sor az alábbi szerkezetű:
(n darab szóköz) . "/" . (k darab szóköz) . "\\<br />"
Itt a . operátor összefűzést jelent, persze ezt így nem írhatjuk be a PHP kódba, először is meg kell határozni n és k értékét, majd ciklussá szervezni a szóközök kiírását. Egy darab backslash-t a "\\" karakterlánccal tudunk kiíratni, aki nem érti hogy miért, az elevenítse fel a Kiíratás című anyagrészt (1. lecke)!
Tegyük fel, hogy n darab szóközt akarunk egymás után kiírni! Először nézzük meg, hogy ezt hogy csináljuk:
// n db szóköz:
$a = 1;
while ($a <= $n){
print " ";
++$a;
}
Remélem, senkinek sem okoz különösebb fejfájást a fenti ciklus, így most nem részletezném. A szóköz helyett a már fent említett kódot kellett használnunk. Már csak annyi a dolgunk, hogy meghatározzuk az előbb említett n és k értékét, és már össze is tudjuk dobni a programot! Ha egy kis ideig nézegetjük a háromszöget, előbb-utóbb talán szemet szúr, hogy a perjel előtti szóközök száma (n) az első sorban ($magassag - 1), majd lefelé haladva mindig eggyel csökken. Fent említettem, hogy a ciklus belsejében fel tudjuk használni azt hogy hányadik sort írjuk, mivel ez van a $sor nevű változóban. Innen egy kis fejtöréssel kitalálhatjuk, hogy n értéke ($magassag - $sor) minden sorban! A perjel és a backslash közti szóközök száma (k) kezdetben 0 (magasságtól függetlenül), és látható hogy lefelé haladva kettesével nő. Ha az első sorban az értéke 2 lenne, akkor azonnal látszana hogy k értéke (2 * $sor), így viszont ebből az értékből mindig le kell vonni 2-t: k = (2 * $sor - 2).
A fentiek alapján a teljes program:
// 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>';
Az $a és $b változók gondoskodnak a ciklusok leállításáról, ezekben tárolódik az addig kiírt szóközök száma. Akár mindkettő lehetne ugyanaz a változó, mivel a második belső ciklusban nincs felhasználva $a értéke, azért adtam nekik külön nevet, mert így jobban látszik hogy a két belső ciklusnak nincs semmi köze egymáshoz.
Ezzel a példával szerettem volna illusztrálni azt az esetet, amikor egy cikluson belül több másikat is elhelyezünk.
Házi feladat
1.) Egészítsük ki a fenti programot a háromszög talpának hozzárajzolásával!
A feladat nagyon hasonló a példaprogramban látható n db szóközt kiíró programrészlethez, csak most szóközök helyett aláhúzás karaktereket kell kiírni. Először kiírunk egy perjelet, majd egy változót használunk a ciklus léptetésére. Szóközök kiírásánál ezt a szerepet az $a és $b változók töltötték be, most legyen egy $talp változó! Miután meghatározott számú aláhúzást kiírtunk, ki kell írni egy backslash-t és kész! A program így néz ki:
// talp:
print "/";
$talp = 1;
while ($talp <= (2 * $magassag - 2)){
print "_";
++$talp;
}
print "\\<br />";
Tulajdonképpen csak annyi volt a feladat, hogy meghatározzuk hogy a $magassag változó függvényében hány aláhúzást kell kiírni. Látható, hogy a háromszög (talpának) szélessége megegyezik a magasság kétszeresével, viszont ebből két karakter a / és \ a többi az aláhúzás. Vagyis (2 * $magassag - 2) darab aláhúzást kell kiírni.
2.) Írassuk ki 1-től 10-ig az összes szám szorzótábláját az előző tananyag 3. feladatához hasonló módon! De az egyes számok szorzótábláit ne egymás alá, hanem egymás mellé írassuk!
(Tipp: használjuk ki a HTML adta lehetőségeket: a szorzótáblákat rakjuk egy táblázat egy-egy celláiba)
Ha lehet akkor most nem térnék ki a táblázatok létrehozásának rejtelmeibe, ez az alapszintű HTML ismeretekhez tartozik. A program váza az alábbi módon néz ki:
print '<table cellpadding="10"><tr>';
$szam = 1;
while ($szam <= 10){
print "<td>";
// $szam szorzótáblája
print "</td>";
++$szam;
}
print '</tr></table>';
A feladat kérésének megfelelően egy sorba (tr) írjuk ki a szorzótáblákat, mindet külön cellába (td). Ezután már csak annyi a dolgunk hogy a megjegyzés helyére beszúrjuk a szorzótáblát létrehozó kódot (ami az előző lecke végén megtalálható):
// Szorzótáblák 1-től 10-ig:
print '<table cellpadding="10"><tr>';
$szam = 1;
while ($szam <= 10){
print "<td>";
$n = 1;
while ($n < 10){
print $n." x ".$szam." = ".($n * $szam)."<br />";
++$n;
}
print "</td>";
++$szam;
}
print '</tr></table>';
3.) Próbáljunk írni egy olyan ciklust, ami meghatározza egy változóról hogy kisebb-e mint 10, és ettől függően kiír egy üzenetet: "Kisebb mint 10" vagy "Nem kisebb mint 10"!
(Ötlet: használhatjuk a ciklusfeltételt a vizsgálatra, de óvakodjunk a végtelen ciklusoktól!)
Ezek szerint a ciklusfeltételnek így kéne kinéznie: ($szam < 10) mivel ezt kell megvizsgálni. A feladatot többféleképpen meg lehet oldani, a legegyszerűbb megoldás alapötlete az, hogy a ciklusfeltétel teljesülése vagy nem teljesülése választja el a két esetet. Ha teljesül, akkor végre lesz hajtva a ciklusmag (pl. kiírjuk, hogy kisebb mint 10), ha nem akkor a ciklus utáni utasításra ugrunk (az pedig kiírja hogy nem kisebb mint 10). A probléma az, hogy a ciklus utáni utasítás akkor is végrehajtódik, ha a ciklusmag lefutott, ezért nem helyezhetünk el olyan utasítást a ciklus után, hogy print "Nem kisebb mint 10"; mivel ez akkor is kiíródna, ha az állítás nem is igaz. A problémát így lehet megoldani:
$szam = 12;
$uzenet = "Nem kisebb mint 10";
while ($szam < 10){
$szam = 10;
$uzenet = "Kisebb mint 10";
}
print $uzenet;
Vagyis a ciklus után egy változó értékét írjuk ki, amit a ciklus előtt beállítunk arra az esetre ha a ciklusfeltétel nem teljesülne. Ha teljesül, akkor a ciklusmag ennek megfelelően megváltoztatja a karakterláncot. Nyilván gondoskodni kell a ciklusból való kilépésről, ezt úgy tehetjük meg, hogy a feltételt hamissá változtatjuk ($szam = 10). Így a fenti példában a ciklusmag csak egyszer fog lefutni.
A program viszonylag rugalmas lett, mivel a 10-es számot akár egy változóban is eltárolhatnánk, és akkor a kódot máshol is felhasználhatnánk. Ezt azonban nem érdemes használni, mert a következő leckében tanulunk egy sokkal jobb módszert a probléma megoldására!







