Pascal programozÁs (A jegyzetet Lutter AndrÁs kÉszÍtette)

1. Mi micsoda?

Ebben a fejezetben megismerkedhetsz a fontosabb alapfogalmakkal és szereplőkkel.

Program és algoritmus

A programozás folyamata során egy feladatot lebontunk pontosan megfogalmazható lépésekre, vagyis elkészítjük a megoldás algoritmusát. Ezt az algoritmust azután olyan nyelven kell megfogalmaznunk, amelyet a számítógép megért. Ez a program. A programot a számítógép végrehajtja, más szóval lefuttatja. Az elkészített programot teszteljük, vagyis kipróbáljuk, működik-e, és azt csinálja-e, amit szeretnénk.

Az algoritmust valamely programozási nyelv szabályainak segítségével kódoljuk programmá. A programozási nyelveket azért fejlesztették ki, hogy ne kelljen a számítógép processzora által megérthető, ám az ember számára  nehezen felfogható gépi nyelven programoznunk. Az általunk megírt programot (forráskód) vagy egy fordítóprogram (compiler) fordítja le gépi nyelvre, vagy egy másik program, az interpreter értelmezi és hajtja végre utasításról utasításra.

A Pascal nyelv jellemzői

Nagyon sok programozási nyelv létezik, ezek között a feladat jellege, saját képességeink és céljaink alapján választhatunk. A Pascal nyelv ugyan kilóg a napjainkban elterjedten használt C-típusú nyelvek (C, C++, C#, Java, Javascript, PHP...) köréből, előnye viszont, hogy kifejezetten a progamozás tanulása/tanítása céljából hozta létre Niklaus Wirth.

A Pascal fordítós nyelv, tehát az általunk megírt Pascal nyelvű forráskódot egy fordítóprogram gépi kódra fordítja (ez Windows rendszeren többnyire egy futtatható .EXE fájl lesz). Sok Pascal fordító létezik, mi a Free Pascal fordítóval fogunk dolgozni, mivel ez ingyenes (sőt, szabadszoftver), és sok platformon elérhető (Windows, Linux, MacOS).

A programozáshoz tehát elegendő lenne egy egyszerű szövegszerkesztő (editor) a forrás elkészítéséhez, és a fordító. Azonban kényelmesebb az IDE (Integrated Developing Environment, Integrált Fejlesztői Környezet) használata. Ebben a fordító mellé kapunk egy editort, mely sokban segíti a kód áttekintését, valamint egy debuggert (hibakereső), mely a futás közben keletkező hibák helyét megmutatja a forrásban.

A Lazarus egy Free Pascalhoz készített IDE, mely lehetővé teszi a gyors alkalmazásfejlesztést (RAD, Rapid Application Development) úgy, hogy a kód egy részét elkészíti helyettünk. Negyedik generációs eszköz, a program egy részét grafikus felületen, egérműveletekkel létre tudjuk hozni. Alapja a Delphi fejlesztői környezet (mely azonban nem szabadszoftver), így a Delphihez készített dokumentációk nagy része használható a Lazarushoz.

 

2. Kezdjük a munkát!

Letöltés és telepítés

Először is le kell töltened a Lazarus rendszert, melyet megtalálsz a lazarus.freepascal.org oldalon. (Download, majd valószínűleg a 32-bites Windows verzióra lesz szükséged.) Ezután telepítsd, ehhez lehet, hogy rendszergazdai jogokra van szükséged a számítógépen. Ne változtass a beállításokon. Mivel a Lazarust folyamatosan fejlesztik, egyre újabb verziók jelennek meg, de a korábbi programokat az újabb verzió is le tudja fordítani.

Beállítások

A magyar billentyűzet miatt néhány beállítást most meg kell tenned. Be kell állítani, hogy a @, [ és { billentyűket ne billentyűparancsnak vegye, mert mindegyik karakterre szükség lesz a programozásnál. Indítsd el a Lazarust. Ekkor egy üres projektet fog létrehozni, de ezzel még nem foglalkozunk. Válaszd ki az Eszközök->Beállítások menüt, azon belül a Szerkesztő->Gyorsbillentyűk beállítást, a jobb oldali listából nyisd ki a Nézet menü parancsai kategóriát, kattints a [Ctrl+Alt+F]-re, és a Törlés gombbal töröld, majd a [Ctrl+Alt+B]-t és a [Ctrl+Alt+V]-t is. OK.

gyorsbillentyűk

Projektek

A forráskód több fájlból állhat, ezek együttesen alkotják a projektet. Ezért általában nem a Fájl, hanem a Projekt menüt használjuk. A fájlok mentésénél célszerű mellőzni az ékezeteket és a szóközt. Egy projektnek mindig külön mappát hozz létre.

Most készítsünk egy projektet! Válaszd a Projekt->Új projekt menüt. Többféle projekt létezik: az Alkalmazás grafikus felületű programot készít, mellyel később foglalkozunk. Válaszd az Egyszerű programot. Ne mentsd a korábbi (üres) projektet.

Láthatod, hogy a Lazarus elkészítette a program vázát. Ezt mentsd el a Projekt->Projekt mentése menüben. Hozz neki létre egy külön mappát, és adj neki nevet, amelyben nem szerepelhet szóköz, sem ékezet. Láthatod, hogy a fájlnevet a Lazarus beleírta a programkódba.
A mappában több fájl is szerepel: az .lpr fájl a forráskód, az .lpi és .lps pedig a projekt beállításait tartalmazza. Később az .lpi fájlon kell duplát kattintanod, ha a Lazarust el akarod indítani a projekt betöltésével. Ha te indítod el a Lazarust, a Projekt->Projekt megnyitása menüben nyithatod meg az .lpi fájlt.
A program fordításakor keletkezik az .exe fájl, mely a forrásfájloktól függetlenül futtatható.

Az első projekt

Zárd be az Objektum felügyelő segédablakot, most nem lesz rá szükség. Az Üzenetek ablak a fordító üzeneteit tartalmazza majd.
Írj valamit az üres projektbe:

program elso; //vagy amilyen nevet adtál a projektednek

begin
  writeln('Szia!');
end.

A // jelek után megjegyzéseket lehet írni, melyeket a fordító figyelmen kívül hagy a sor végéig.
A főablak eszköztárán nyomd meg a zöld nyilat. Ez lefordítja, és le is futtatja a programot. Fordítás előtt mindig ment, az első mentés után tehát nem kell mindig mentened a programodat.
A futó programod egy rövid villanás után bezárul, és a kiírt adatok eltűnnek. Ezért módosítjuk   úgy, hogy az Enter lenyomásáig ne érjen véget.

begin
  writeln('Szia!');
  readln;
end.       

A program a Windowsban létrehoz egy szöveges ablakot (konzolt), abba kiírja a szöveget. Ezután vár az Enter lenyomásáig, majd véget érvén bezárja a konzol ablakot. Láthatod, hogy amíg a program nem fejeződik be, a zöld futattás helyett piros stop gomb van a főablakban.

Írj be hibás programot, mondjuk a
writeln helyett witeln-t. Futtasd le! A fordító most nem fordítja le  a programot. Ehelyett "Identifier not found" hibajelzést ad.  Ennek jelentése: ismeretlen azonosító. A fordító számára minden azonosító, aminek jelentése van. Az azonosítók betűvel kezdődnek, majd ékezetmentes betűt, számot, aláhúzásjelet tartalmazhatnak. Az azonosítók értelmezésekor a fordító nem különbözteti meg a kis- és nagybetűket (írhattunk volna WriteLN-t is).

Miért nem működnek az ékezetes karakterek?

Módosítsd a fenti programot, pl. így:

writeln('szőlőlekvár');

és meglepődve láthatod a konzolablakban lévő kusza karakterhalmazt. Ennek oka a Lazarus és a konzolablak eltérő karakterkódolása. A Lazarus forrásszerkesztője a karaktereket utf-8 kódolás szerint tárolja, míg a konzolablak (legalábbis a magyar Windows XP-n) a 852-es kódlapot használja. A megoldás az, hogy a forrásszerkesztőt átállítjuk: ékezetes karakter bevitelekor a forráskódba a karakter 852-es kódlap szerinti kódját helyezze el (és így is jelenítse meg a kódot). Kattints a jobb gombbal a forráskód ablakába, és a Fájl beállítások->Kódolás menüből válaszd ki a CP852-es kódlapot. Igen, alakítsa át a fájlt.

Ezt a lépést csak akkor kell megtenned, ha a forrásszerkesztőben bevitt karaktereket konzolablakba szeretnéd kiírni.

 

3. Egyszerű programok kifejezésekkel

Utasítás, paraméter, kifejezés

Észrevehetted, hogy a program begin és end szavak között helyezkedik el, és a végét pont jelzi. A program utasítások (parancsok) egymás utánja, melyeket pontosvessző választ el. Írd be a következő programot (vagyis módosítsd az eredeti projektet):

program project1;
begin
  write('ablak')
  write('zsiráf')
  readln;
end.

Fordításkor a "; expected" hibajelzést kapod, ami azt jelenti, hogy valahonnan hiányzik a pontosvessző. (Kezdő pascalos leggyakoribb hibája.) Pótold ezeket, és nézd meg, mit ír ki a program!
Ezután írd át a
write parancsokat writeln-re. Láthatod, hogy az utóbbi parancs annyiban különbözik az előzőtől, hogy kiírás után új sorba áll.

A write és writeln utasítások után zárójelben a kiírandó dolog áll. Azt, hogy az utasítás mire vonatkozik, paraméternek nevezzük.  A writeln utasításnak több paramétere is lehet, melyeket vesszővel választunk el, pl. writeln('záp','tojás')


Most nézzük a következő programot:

program project1;
begin
  writeln('3+4');
  writeln(3+4);
  readln;
end.

Látható, hogy az egyszeres idézőjel (aposztróf) szöveget jelöl, míg anélkül a Pascal kiszámítja a művelet eredményét. Mindent, aminek értéke van, kifejezésnek nevezünk. A program futás közben kiszámítja a kifejezések értékét. Pl.

writeln('5*4','=',5*4)

A kifejezésekben szövegeken és számokon kívül lehetnek műveleti jelek és zárójelek (gömbölyű) is.

Típusok

A kifejezések típusa megadja, hogy milyen értékeket vehet fel egy kifejezés, és milyen műveleteket lehet vele végezni. A Pascal sokféle típust használ. Ezek közül a legfontosabbak, Pascal azonosítójukkal:

  • egész szám (integer): pl. 5, 3+4
  • szöveg (string): pl. 'zsiráf'
  • karakter (char): pl. 'N'
  • törtszám (real) pl. 0.75, 3/4

A writeln(3/4) szokatlan módon írja ki a 0,75-öt: 7.500000E-0001. Ez a szám tízes normálalakja (7,5·10-1), és praktikus, ha nagyon nagy vagy kis számot akarunk leírni (pl. 6·1023, Pascalul 6E+23). De nem kötelező használnunk: ekkor kiírásnál megadjuk a szám teljes szélességét, és hogy hány tizedesre akarjuk kerekíteni, pl. writeln(1/3:5:3)

Mit ír ki ez az utasítás? writeln(3<7,5=6)

Az ilyen kifejezések csak igaz (TRUE) vagy hamis (FALSE) értéket vehetnek fel, a típus neve:

  • logikai (boolean): pl. TRUE, 3>7

Műveletek

A matematikai alapműveleteken kívül (+ - * /) fontosak még a következők:

  • szövegösszefűzés: pl. 'záp'+'tojás' eredménye 'záptojás'
  • egészek osztási maradéka: pl. 12 mod 5 eredménye 2
  • egészek hányadosa: pl. 25 div 7 eredménye 3
  • logikai és: pl. (3>4) and (5=5) eredménye FALSE
  • logikai vagy: pl. (3>4) or (5=5) eredménye TRUE
  • logikai tagadás: pl. not TRUE eredménye FALSE

Függvények

A függvényeknek az utasításoktól eltérően visszatérési értékük is van. Pl. a writeln(sqr(3)) utasítás 9-et ír ki, mert az sqr függvény a négyzetre emelés. Próbálj ki néhány fontos függvényt:

begin
  writeln(sqr(3));
  writeln(sqrt(25));
  writeln(round(3.65));
  writeln(trunc(3.65));
  readln;
end.  

 

Foglaljuk össze az eddig megismert Pascal azonosítókat:

  • a program szerkezetét adják meg: begin, end
  • utasítások: write (kiír), writeln (kiír és új sorba áll), readln (Enterre vár)
  • függvények: sqr (négyzet), sqrt (négyzetgyök), round (kerekítés), trunc (csonkítás)
  • műveletek: +, -, *, /, mod, div, and, or, not, <, >, =, <= (kisebb vagy egyenlő), >= (nagyobb vagy egyenlő), <> (nem egyenlő)

Feladatok

1. Írasd ki a következő dolgokat. Ami programmal kiszámítható, azt a program számítsa ki!

  • 22 négyzetgyökének négyzetét
  • azt, hogy: 33*77=2541
  • ezt (vesszőkkel együtt): 2+2=4, 4+4=8, 8+8=16
  • annak a téglatestnek a felszínét, amelynek oldalai 3, 4, 5
  • annak a derékszögű háromszögnek az átfogóját, amelynek befogói 5, 11
  • azt a szöveget, hogy: Writeln('Szia!'); (a kiírásban két aposztrófot tegyél egymás mellé)
  • ezt: 1/50=0.02

megoldás

BEGIN
 Writeln(sqr(sqrt(22)));
 Writeln('33*77=',33*77);
 Writeln('2+2=',2+2,', 4+4=',4+4,', 8+8=', 8+8);
 Writeln(2*(3*4+3*5+4*5));
 Writeln(sqrt(sqr(5)+sqr(11)):0:2);
 Writeln('Writeln(''Szia!'');');
 Writeln('1/50=',1/50:4:2);
 Writeln('Nyomj ENTER-t!'); Readln;
END.

 

4. Változók

A program futása közben a memóriában adatokat tud tárolni, változók segítségével. Egy változónak van neve, típusa és értéke. Az értékét lehet változtatni. A programban a változó neve is egy azonosító.

Változók deklarációja és használata

Nézzük meg a következő programot! (A weboldalról a Vágólap segítségével be tudod másolni a kódot a projektedbe, nem kell begépelni!)

Var a,b:integer;
    c:real;
BEGIN
 a:=5;
 b:=3;
 c:=a/b;
 writeln(a,'/',b,'=',c:5:3);
 a:=a+1;

 writeln(a);
 readln;
END.

Először is, figyeld meg a programban a behúzások használatát. Amikor elkezdünk valamit, az alatta lévő sorokat beljebb kezdjük (szóköz segítségével). Ennek szerepe a forrás áttekinthetőségének javítása. Bár a fordítónak mindegy (a szóközökkel, sorvégekkel nem foglalkozik), nekünk nagyon fontos, hogy kiigazodjunk egy hosszabb kódban.

A programot kezdő begin előtt találjuk a deklarációs részt. Deklaráció az, amikor egy új azonosítót mutatunk be a fordítónak. Ennek hiányában az a:=5 utasításra "ismeretlen azonosító" hibajelzéssel leállna a fordítás.

A deklarációs részben a var vezeti be a változók deklarálását. Ebben szerepel a változók neve és típusa. A Pascal ilyenkor statikus memóriakezelést alkalmaz, vagyis a változóknak előre lefoglalja a megfelelő memóriaterületet.

A változóknak értéket a := (legyen egyenlő) utasítással adunk. Baloldalt a változó, jobboldalt egy kifejezés van. Így az a:=a+1 eggyel nagyobb értéket tárol az a változóban, mint ami előtte volt – tehát megnöveli eggyel a változóban tárolt számot.

Változók értékének beolvasása

A fenti program csak egyféle eredményt ír ki. Most próbáld ki a következő programot:

VAR a,b:integer;
BEGIN
  Write('Add meg az egyik számot: '); Readln(a);
  Write(' Add meg a másik számot: '); Readln(b);
  Writeln('A két szám összege: ',a+b);
END.

A readln utasításnak paraméterként most egy változót adtunk át. Ilyenkor a readln addig várakozik, míg begépelünk egy értéket és entert nyomunk, ekkor az értéket eltárolja a változóba. Így tehát a felhasználó a program futása közben tudja megadni a változók értékét.

A kiírásra azért van szükség a beolvasás előtt, hogy a felhasználó tudja, mit vár tőle a program: egyébként csak üres képernyőt látna.

Feladatok

2. Írj programot, amely beolvassa egy derékszögű háromszög befogóinak hosszát, és kiírja az átfogó hosszát!

megoldás

VAR a,b:real;
BEGIN
 Write('Egyik befogó: '); Readln(a);
 Write('Másik befogó: '); Readln(b);
 Writeln('Az átfogó: ',sqrt(sqr(a)+sqr(b)):0:2);
 Writeln('Nyomj ENTER-t!'); Readln;
END.


3. Írj programot, amely beolvassa egy téglatest oldalainak hosszát, és kiírja a téglatest felszínét és térfogatát!
megoldás

VAR a,b,c:real;
BEGIN Write(' Egyik oldal: '); Readln(a);
 Write(' Másik oldal: '); Readln(b);
 Write('Harmadik oldal: '); Readln(c);
 Writeln('A felszín: ',2*(a*b+a*c+b*c):0:2);
 Writeln('A térfogat: ',a*b*c:0:2);
 Writeln('Nyomj ENTER-t!'); Readln;
END.


4. Írj programot, amely beolvassa a felhasználó nevét, és névre szólóan üdvözli!
megoldás

VAR s:string;
BEGIN
 Write('Hogy hívnak? '); Readln(s);
 Writeln('Üdvözöllek, kedves '+s+'!');
 Writeln('Nyomj ENTER-t!'); Readln;
END.


5. Írj programot, amely beolvas 3 számot, majd kiírja, teljesül-e, hogy a középső szám nagyobb a két szélsőnél (true/false formában)!

megoldás

VAR a,b,c:integer;
BEGIN
 Write('Adj meg 3 számot!'); Readln(a,b,c);
 Writeln((b>a) and (b>c));
 Writeln('Nyomj ENTER-t!'); Readln;
END.

 

 

5. Elágazás

Az elágazás azt jelenti, hogy egy feltételtől függően a programnak az egyik vagy másik része fut le. A következő program a beírt két szám közül a nagyobbikat írja ki.

Var a,b:integer;

BEGIN

  write('Írj be két számot: '); readln(a,b);

  if a>b then writeln(a) else writeln(b);

  readln;

END.

 

Az elágazás tehát: if feltétel then utasítás else utasítás;

A feltétel valójában egy boolean típusú kifejezés. Ha igaz, akkor a then, egyébként az else utáni utasítás hajtódik végre. Fontos megfigyelni, hogy az else előtt nincs pontosvessző, mert az elágazás még nem fejeződött be.  A fenti programban látható, hogyan lehet egy readln-nel több számot is beolvasni. A két számot szóközzel elválasztva kell begépelni.
Ha az első utasítás után pontosvessző van:

if a=b then writeln('Egyenlők!');

akkor az elágazás lezárul, vagyis ha a feltétel nem teljesül, semmit nem hajt végre a program.

Összetett utasítás

Ha a then vagy else ágban több utasítást szeretnénk végrehajtani, azokat nem írhatjuk be közvetlenül, hanem csak egy utasításként. Több utasítást egyetlen összetett utasítássá alakíthatunk, ha azokat begin és end közé tesszük.

Var s:string;
BEGIN
  write('Írd be a neved: '); readln(s);
  if s<>'' then begin
    writeln('**********');
    writeln('Szia ',s);
    writeln('**********');
    readln;
  end;
END.

Itt a két egymás melletti aposztróf üres szöveget jelöl.

Összetett feltételek

A feltételek között használhatjuk az és, vagy műveleteket. Fontos tudni azonban, hogy milyen sorrendben dolgozza fel a Pascal a műveleteket, hogy  a megfelelő helyeken zárójeleket használjunk.

Var a,b,c:integer;
BEGIN
  write('Írj be három számot: '); readln(a,b,c);
  write('A legnagyobb=');
  if (a>b) and (a>c) then writeln(a);
  if (b>=a) and (b>c) then writeln(b);
  if (c>=a) and (c>=b) then writeln(c);
  readln;
END.

Azért használtunk >= műveletet is, hogy mindenképpen egy számot írjon ki a program, függetlenül attól, hány egyforma van a beírtak között.

Egymásba ágyazott feltételek

Az elágazásban lehet további elágazás is. Ha nem ügyelsz a behúzásokra, áttekinthetetlen lesz a programod!

var s:string;
BEGIN
  write('Fiú vagy? '); readln(s);
  if s='igen' then begin
    write('Szeretsz focizni? '); readln(s);
    if s='igen' then writeln('Barátkozz Józsival, neki van labdája.')
    else writeln('Barátkozz Sanyival, neki van Xboxa.');
  end else begin
    write('Szereted a lovakat? '); readln(s);
    if s='igen' then writeln('Barátkozz Julival, neki jár a Penny Girl.')
    else writeln('Barátkozz Sárival, ő szeret ruhákról beszélgetni.');
  end;
  readln;
END. 

Feladatok

6. Írj programot, mely a beolvasott út és idő értékéből kiszámítja az átlagsebességet. Az algoritmus:

Be: út, idő {persze a változóidat nem így nevezed el}
Ha út<0 VAGY idő<=0 akkor {vigyázz a zárójelezésre!}
  ki: "Értelmetlen adatok!"
különben
  ki: út / idő

megoldás

VAR s,t:real;
BEGIN
 Write('Add meg az utat (m) és az időt (s): '); Readln(s,t);
 If (s<0) or (t<=0) then writeln('Értelmetlen adatok!')
 else writeln('Az átlagsebesség: ',s/t:0:2,' m/s');
 Writeln('Nyomj ENTER-t!'); Readln;
END.


7. Írj programot, mely a megadott számról eldönti, hogy páros vagy páratlan! (Emlékeztető: MOD.)

megoldás

VAR a:integer;
BEGIN
 Write('Adj meg egy egész számot: '); Readln(a);
 If a mod 2 = 0 then writeln('Páros.')
 else writeln('Páratlan.');
 Writeln('Nyomj ENTER-t!'); Readln;
END.


8. Írj programot, mely a beadott számról eldönti, hogy pozitív, negatív vagy nulla-e! (Háromszoros elágazás.)

megoldás

VAR a:integer;
BEGIN
 Write('Adj meg egy egész számot: '); Readln(a);
 If a > 0 then writeln('Pozitív.')
 else If a=0 then writeln('Nulla.')
      else writeln('Negatív.');
 Writeln('Nyomj ENTER-t!'); Readln;
END.


9. Írj programot, mely megadja, hogy 3 beolvasott (integer) számból hány egyforma volt (mind különböző / 2 egyforma / 3 egyforma)!

megoldás

VAR a,b,c:integer;
BEGIN
 Write('Adj meg három egész számot: '); Readln(a,b,c);
 If (a=b) and (b=c) then writeln('3 egyforma.')
 else if (a=b) or (a=c) or (b=c) then writeln('2 egyforma.')
      else writeln('Nincs egyforma.');
 Writeln('Nyomj ENTER-t!'); Readln;
END.

 

 

6. Ciklusok

A program többször végrehajtódó részét ciklusnak nevezzük. A ciklusfeltétel adja meg, mikor kell a ciklusmagot ismét végrehajtani.

Elöltesztelős ciklus

A következő program megkeresi a legkisebb 15-nél nagyobb, 7-tel osztható páros számot. Az oszthatóságot a mod művelet segítségével ellenőrizzük.

Var a:integer;
BEGIN

   a:=16;
   while (a mod 7>0) or (a mod 2>0) do a:=a+1;
   writeln(a);
   readln;
END.  

A ciklus felépítése: while feltétel do utasítás;

Azért elöltesztelős, mert először vizsgálja a feltételt és utána hajtja végre a ciklusmagot, ha a feltétel igaz. Lehet, hogy egyszer sem fut le (ha a feltétel már kezdetben hamis).

A fenti program úgy működik, hogy addig növeli a értékét, amíg nem felel meg a feladat feltételének (vagyis amíg nem osztható 7-tel vagy nem osztható 2-vel). Ezért ha leáll, akkor azért áll le, mert a osztható 7-tel és osztható 2-vel is.

A do után csak egy utasítás állhat, ezért több utasításból álló ciklusmagot begin és end közé kell írni.

Hátultesztelős ciklus

A hátultesztelős ciklus először hajtja végre a ciklusmagot és utána vizsgálja a feltételt – ezért a ciklusmag legalább egyszer lefut.

Var a:integer;
BEGIN
   repeat
     write('Adj meg egy egész számot, 0-ra kilép: '); readln(a);
     if a mod 11=0 then writeln('osztható 11-gyel.')
     else writeln('nem osztható 11-gyel.');
   until a=0;
END.

Ennél a ciklusnál repeat és until közé több utasítást is be lehet írni, az until után pedig a leállás feltételét kell megadni.

Most nem tettünk a program végére readln-t, mert a felhasználó 0 beírásával már jelezte, hogy ki akar lépni.

Számlálós ciklus

Gyakori feladat, hogy egy változóval egyesével számolunk:

Var i:integer;
BEGIN
  i:=1;
  while i<=10 do begin
    writeln(i:2,' négyzete=',sqr(i):3);
    i:=i+1;
  end;
  readln;
END.    

Itt a :2 azt jelzi a kiírásnál, hogy az egész számot hány karakter szélességben kell jobbra igazítva kiírni (balról szóközökkel kiegészítve); így a megfelelő helyiértékek egymás alá kerülnek. Így jobban néz ki.


A számlálós (vagy for-) ciklus pont ezt csinálja:

Var i:integer;
BEGIN
  for i:=1 to 10 do writeln(i:2,' négyzete=',sqr(i):3);
  readln;
END.

A ciklus így néz ki: for változó:=kezdőérték to végérték do utasítás;

ahol a változót egyesével növeli a kezdőértéktől a végértékig, és minden lépésben végrehajtja az utasítást.

Mivel a ciklus elöltesztelős, ha a kezdőérték nagyobb a végértéknél, egyszer sem fut le. Ilyenkor viszont to helyett downto-t használva visszafelé számol.

Mivel ennek a ciklusnak a beírása nagyon rövid, gyakran a nem egyesével számláló feladatokat is így csináljuk meg.  A következő program páratlan számokat ír ki:

Var i:integer;
BEGIN
  for i:=1 to 10 do writeln(2*i-1);
  readln;
END.

 

Használhatjuk akkor is, ha egyszerűen csak valahányszor meg akarunk csinálni valamit.

Var fokozat,i:integer;
BEGIN
  write('Mennyire szépen kérjem? '); readln(fokozat);
  for i:=1 to fokozat do write('nagyon ');
  writeln('szépen kérem!');
  readln;
END.   

Példa ciklusra és elágazásra

Ez egy számkitalálós játék, ahol a gondolt számot a gép a random függvénnyel állítja elő. Pl. random(4) eredménye egy 0 és 3 közötti véletlenszerűen választott egész szám. Természetesen a gép igazi véletlen eseményt nem tud létrehozni (nem dobhat kockával), és ezeket a számokat meghatározott algoritmus alapján állítja elő. Ha a programban kiadjuk a randomize parancsot, minden futtatáskor más véletlen sorozatot kapunk.

VAR g,t,c:integer;

BEGIN
  randomize;
  g:=random(100)+1;
  c:=0;
  repeat
    write('Gondoltam egy számot 1 és 100 között. Találgass: ');
    readln(t);
    if g<t then writeln('Kisebbre gondoltam!');
    if g>t then writeln('Nagyobbra gondoltam!');
    c:=c+1;
  until g=t;
  writeln('Eltaláltad ',c,' lépésben!');
  writeln('Nyomj entert'); readln;
END.

Egymásba ágyazott ciklusok

Ciklusban ciklus is lehet. Ha a külső ciklus n-szer fut le, a belső pedig m-szer, akkor a belső ciklus ciklusmagja összesen n·m alkalommal fog lefutni. A következő példa belső ciklusa 15 db. '*' karaktert ír ki egy sorba, amit a külső ciklus 10-szer ismétel. Az eredmény egy 150 *-ból álló téglalap.

Var i,j:integer;
BEGIN
 for i:=1 to 10 do begin
   for j:=1 to 15 do write('*');
   writeln;
 end;
 writeln('Nyomj entert'); readln;
END.

Feladatok

10.  Írj programot, amely kiírja a 0, 5, 10, ..., 100 számokat (ötösével) elöltesztelős ciklussal

megoldás

VAR a:integer;
BEGIN
 a:=0;
 While a<=100 do begin
   writeln(a);
   a:=a+5;
 End;
 Writeln('Nyomj ENTER-t!'); Readln;
END.


11. Írj programot, amely kiírja a 0, 5, 10, ..., 100 számokat (ötösével) hátultesztelős ciklussal!

megoldás

VAR a:integer;
BEGIN
 a:=0;
 Repeat
   writeln(a);
   a:=a+5;
 Until a>100;
 Writeln('Nyomj ENTER-t!'); Readln;
END.


12. Írj programot, amely a következő idétlen játékot játssza a felhasználóval mindaddig, amíg az a "nem" szót beírja. A felhasználó által beírt dolgokat vastagítva jelzem.
     Írj be egy számot!
    6
    7! Nyertem! Még egy játék?
    igen
    Írj be egy számot!
    2000
     2001! Nyertem! Még egy játék?
    nem
    Kösz a játékot!

megoldás

VAR a:integer;
    s:string;
BEGIN
 Repeat
   Writeln('Írj be egy számot!'); Readln(a);
   Writeln(a+1,'! Nyertem! Még egy játék?'); Readln(s);
 Until s='nem';
 Writeln('Kösz a játékot!');
 Writeln('Nyomj ENTER-t!'); Readln;
END.


13. Írj programot, amely kiszámítja az 1 és 100 közötti egész számok összegét

megoldás

VAR a,s:integer;
BEGIN
 s:=0;
 For a:=1 to 100 do
   s:=s+a;
 Writeln(s);
 Writeln('Nyomj ENTER-t!'); Readln;
END.


14. Írj programot, amely megadja az 1 és 1000 közötti, 7-tel osztható számok összegét!

megoldás

VAR a:integer;
    s:real;
BEGIN
 s:=0;
 a:=7;
 While a<=1000 do begin
   s:=s+a;
   a:=a+7;
 End;
 Writeln(s:0:0);
 Writeln('Nyomj ENTER-t!'); Readln;
END.

//másik megoldás a ciklusra:
For a:=1 to 1000 do
  if a mod 7=0 then s:=s+a;


15. Írj programot, amely kiszámítja 1,13-nak a 113-adik hatványát (szorzásokkal)!

megoldás

VAR i:integer;
    p:real;
BEGIN
 p:=1;
 For i:=1 to 113 do
   p:=p*1.13;
 Writeln(p:0:3);
 Writeln('Nyomj ENTER-t!'); Readln;
END.


16. Írasd ki (FOR-ciklussal) 1 és 1000 között a 7-tel osztható, páratlan számokat! (Tipp: egyesével számolunk, de csak a feltételnek megfelelő számokat írjuk ki.)

megoldás

VAR a:integer;
BEGIN
 for a:=1 to 1000 do
   if (a mod 7 = 0) and (a mod 2 > 0) then write(a:5);
 Writeln;
 Write('Nyomj ENTER-t!'); Readln;
END.


17. Írasd ki az 5-ös szorzótáblát: 1*5=5, 2*5=10, ..., 10*5=50!

megoldás

VAR a:integer;
BEGIN
 For a:=1 to 10 do
   writeln(a,'*5=',a*5);
 Write('Nyomj ENTER-t!'); Readln;
END.


18. Írasd ki a teljes 10-es szorzótáblát 1*1-től 10*10-ig! (Tipp: egymásba ágyazott ciklusok.)

megoldás

VAR a,b:integer;
BEGIN
 For a:=1 to 10 do begin
   For b:=1 to 10 do write(a,'*',b,'=',a*b,' ');
   Writeln;
 End;
 Write('Nyomj ENTER-t!'); Readln;
END.

 

 

 

7. Tömbök

A tömb a programozás során használt egyik leggyakoribb adatszerkezet. Egy adatszerkezet adatokat tárol, és lehetővé teszi az adatokhoz való hozzáférést. A tömb elemei változók, melyekhez a tömbön belül sorszámukkal, más néven indexükkel férhetünk hozzá. Ha t egy tömb, akkor az elemei lehetnek t[1], t[2] stb. Megjegyezzük, hogy míg sok programozási nyelvben a tömbök indexe 0-val kezdődik, a Pascalban ez nem feltétel.

Tömbök deklarálása és indexelése

Tömbök deklarálásánál meg kell adnunk az indexhatárokat, és hogy a tömb elemei milyen típusúak. Egy tömb minden eleme azonos típusú. A következő példa egy 10-elemű t tömböt deklarál, melynek elemei egészek. A tömb típusa array.

Var t:array[1..10] of integer;

BEGIN

 t[1]:=3;

 t[2]:=t[1]+1;

END.

A tömb típus tehát: array [mettől..meddig] of alaptípus

A tömb egy elemére tömbnév[index] formátumban hivatkozunk. A fenti példában t[2] egy integer típusú változó, a tömb második eleme, az index 2.

Hibák és kezelésük

Futtasd most le az első mintaprogramot úgy, hogy az egyik indexet 1-ről 11-re módosítod! A fordító "range check error" üzenetet ad (fordítási hiba). Ez azt jelenti, hogy a tömbnek annyiadik elemére hivatkoztunk, ahányadik nincs. Ez a program futtatásakor súlyos hibát okozna. A fordító lefoglal valamekkora memóriaterületet a tömbnek, a hibás indexű elem ebből kilóg, máshoz tartozó adatokat módosíthat. Ebben a példában a fordító észlelte a hibát, de ez gyakran csak a program futtatásakor derülhet ki. A következő példaprogram hiba nélkül lefordul:

Var t:array[1..10] of integer;
    i:integer;
BEGIN
 for i:=1 to 11 do t[i]:=0;
END.

...ami nagy baj, mert futtatáskor a memória szabálytalan használata változatos hibajelenségeket okozhat! Ezért a fordítót be kell állítanunk, hogy a program futás közben figyelje ezt a hibatípust. A Projekt->Projekt beállítások... menüből válaszd ki a Fordító beállításai->Hibakeresés pontot, és ott jelöld be a Tartomány négyzetet.

fordító beállítás

 

Az első 4 opció mind kipipálandó. A Hibakeresési információk létrehozása szerepe az, hogy a tárgykódba a fordító beírja, az egyes parancsok a forráskód hányadik sorában vannak. Ez futás közben fellépő hibáknál (futási hiba) nagyon hasznos segítség, az IDE megmutatja a hiba helyét a forráskódban. (A tárgykód, vagyis az .exe fájl mérete ugyanakkor a tízszeresére nő. Ezért a program végső, terjeszthető változatát e nélkül a kapcsoló nélkül fordítsuk.)

Most futtasd le a fenti hibás programot! A futás leáll, 201-es hibakóddal (ez a range check error futási hiba kódja). Válaszd  a Megszakítást, majd nyomd meg a piros négyzetet.

Az I/O hibáknak fájlkezelésnél van szerepe, a Túlcsordulásnak pedig akkor, ha egy változóban túl nagy értéket akarunk tárolni. E nélkül túlcsordulás esetén nem hibaüzenetet kapunk, hanem a program hibás értékkel fut tovább.

Tömb feldolgozása ciklussal

Sok tömbös feladatnál for-ciklust használunk arra, hogy a tömb elemein végigmenjünk. Ez a ciklus 0 kezdőértékkel tölt fel egy tömböt:

for i:=1 to 10 do t[i]:=0;

 

A következő összetett program beolvassa 10 ember nemét és magasságát, majd kiírja külön a lányok és külön a fiúk átlagos magasságát. A NEM tömb tárolja a nemet (f/l), a MAG tömb a magasságot.


Var NEM:array[1..10] of char;
    MAG:array[1..10] of integer;
    i:integer;
    dbfiu,dblany,mfiu,mlany:integer;
BEGIN
 for i:=1 to 10 do begin
   write(i,'. ember neme (f/l): '); readln(NEM[i]);
   write(i,'. ember magassága (egész cm): '); readln(MAG[i]);
 end;

 dbfiu:=0; dblany:=0;

 mfiu:=0; mlany:=0;
 for i:=1 to 10 do
   if NEM[i]='f' then begin
     dbfiu:=dbfiu+1;
     mfiu:=mfiu+MAG[i];
   end else begin
     dblany:=dblany+1;
     mlany:=mlany+MAG[i];
   end;
 if dbfiu>0 then writeln('Fiúk   átlagmagassága=',mfiu/dbfiu:5:1,' cm');
 if dblany>0 then writeln('Lányok átlagmagassága=',mlany/dblany:5:1,' cm');
 write('Nyomj entert!'); readln;
END.    

Az első ciklus a beolvasás, a második a feldolgozás. (A kettőt össze is lehetett volna vonni, sőt, a feladatot tömbök nélkül is meg lehetett volna oldani.)

A tömbök mérete

Hogyan kell a tömböket deklarálni akkor, ha nem tudjuk előre, mennyi adatot kell tárolni? A statikus memóriakezelés miatt a tömb méretét már a deklarációs részben meg kell adni, futás közben nem lehet megnövelni. Ezért akkora tömböt kell létrehozni, amelyben biztosan elfér minden szükséges adat. Érdemes lehet a programban azt is kezelni, hogy mi van, ha mégsem.

A következő két példa kétféleképpen oldja meg legfeljebb 10 pozitív szám beolvasását. A tömb deklarációja (innentől kezdve egyre gyakrabban csak programrészleteket adok meg):

var t:array [1..10] of integer;

 

Ez a megoldás először megkérdezi a számok számát.

write('Hány szám legyen (legfeljebb 10): '); readln(db);

if db>10 then writeln('Érvénytelen érték!')
else
  for i:=1 to db do begin

    write('Add meg a(z) ',i,'. számot: '); readln(t[i]);

  end;


Kényelmesebb, ha a felhasználónak nem kell előre tudnia, hány számot fog beírni. Ekkor valahogy jeleznie kell a beírás végét. Mivel most pozitív számokat kér a program, a 0 beírása alkalmas erre.

db:=0;

repeat

  write('Add meg a(z) ',db+1,'. számot, 0=vége: '); readln(a);

  if a>0 then begin

    db:=db+1;

    t[db]:=a;

  end;

until (a=0) or (db=10);

Többdimenziós tömbök

Egy tömbelem lehet tömb is, ekkor a tömb valahányadik elemének is lehet valahányadik eleme. Míg az egydimenziós tömböt értékek sorozataként szemlélhetjük, a kétdimenziós tömb már táblázat sorokkal, oszlopokkal, és egy tömbelem indexelésekor a sort és oszlopot is megadjuk, pl. t[4,2]. Egy kétdimenziós, 5x2-es tömb így deklarálható:

var t:array[1..5,1..3] of integer;

A három- vagy többdimenziós tömböket már nehezebb szemléletesen elképzelni.

 

Ez a példa előállítja, majd kiírja a következő táblázatot, ahol az a szabály, hogy minden szám a táblázatban fölötte és tőle balra elhelyezkedő számok összege. A bal szélen és a tetején elhelyezkedő minden szám 1.
 1  1  1  1  1
 1  2  3  4  5
 1  3  6 10 15

Először is tisztázni kell, melyik index melyik. Legyen a j. sor i. oszlopában lévő szám t[i,j]. A többdimenziós tömböt jellemzően egymásba ágyazott ciklusokkal dolgozzuk fel.

Var t:array [1..5,1..3] of integer;
    i,j:integer;
BEGIN
 //első sor és első oszlop kezdőértékei
 for i:=1 to 5 do t[i,1]:=1;
 for j:=1 to 3 do t[1,j]:=1;

 //a többi érték kiszámítása

 for i:=2 to 5 do
   for j:=2 to 3 do t[i,j]:=t[i-1,j]+t[i,j-1];

 //a táblázat kiírása

 for j:=1 to 3 do begin
   for i:=1 to 5 do write(t[i,j]:3);
   writeln;
 end;

 write('Nyomj entert!'); readln;

END.     

Az első két ciklus nem egymásba ágyazott, mert azok csak egy soron vagy egy oszlopon mennek végig. Amikor egy elem értéket kap a harmadik ciklusban, akkor ezt olyan elemek alapján kapja, amelyek már korábban értéket kaptak. A kiíró ciklusban a :3 miatt mindegyik számot 3 karakter szélességben írja ki, így a táblázat szabályos lesz az egy- és kétjegyű számok esetén is.

Feladatok

19. Írj programot, amely 1 és 6 közti véletlenszámokkal feltölt egy tömböt, majd kiírja, hány páros szám van benne!
megoldás

VAR t:array[1..10] of integer;
    i,c:integer;
BEGIN
 for i:=1 to 10 do t[i]:=random(6)+1;
 c:=0;
 for i:=1 to 10 do if t[i] mod 2 = 0 then c:=c+1;
 writeln(c,' páros szám volt.');
 write('Nyomj entert!'); readln;
END.


20. Írj programot, amely 1 és 6 közti véletlenszámokkal feltölt egy tömböt, és kiírja a benne lévő páros számok összegét!
megoldás

VAR t:array[1..10] of integer;
    i,c:integer;
BEGIN
 for i:=1 to 10 do t[i]:=random(6)+1;
 c:=0;
 for i:=1 to 10 do if t[i] mod 2 = 0 then c:=c+t[i];
 writeln('A páros számok összege: ',c);
 write('Nyomj enter!');
 readln;
END.


21. Írj programot, amely 1 és 6 közti véletlenszámokkal feltölt egy tömböt, majd kiírja a benne lévő páros számokat!
megoldás

VAR t:array[1..10] of integer;
    i:integer;
BEGIN
 for i:=1 to 10 do t[i]:=random(6)+1;
 for i:=1 to 10 do if t[i] mod 2 = 0 then writeln(t[i]);
 write('Nyomj enter!');
 readln;
END.


22. Írj programot, amely 1 és 6 közti véletlenszámokkal feltölt egy tömböt, majd kiírja, hány alkalommal követ egy számot vele egyenlő! (Például a 4 2 4 4 3 2 2 2 6 sorozatnál ez a szám 3.)
megoldás

VAR t:array[1..10] of integer;
    i,c:integer;
BEGIN
 for i:=1 to 10 do t[i]:=random(6)+1;
 c:=0;
 for i:=1 to 9 do if t[i]=t[i+1] then c:=c+1;
 writeln('Ismétlések száma: ',c);
 write('Nyomj enter!'); readln;
END.


23. Írj programot, amely bekéri emberek keresztnevét és életkorát, majd kiírja a 10 évnél fiatalabbak nevét!
megoldás

VAR nev:array[1..10] of string;
    kor:array[1..10] of integer;
    i,db:integer;
    s:string;
BEGIN
 db:=0;
 repeat
   write(db+1,'. ember neve, *=kilépés: '); readln(s);
   if s<>'*' then begin
     db:=db+1;
     nev[db]:=s;
     write(db,'. ember kora: '); readln(kor[db])
   end;
 until (s='*') or (db=10);
 writeln;
 writeln('10 évnél fiatalabbak:');
 for i:=1 to db do if kor[i]<10 then writeln(nev[i]);
 write('Nyomj enter!'); readln;
END.


24. Írj programot, amely véletlenszámokkal feltölt egy 5x10-es tömböt, majd kiírja, hogy páros vagy páratlan számból volt-e benne több!
megoldás

VAR t:array [1..5,1..10] of integer;
    i,j,p:integer;
BEGIN
 for i:=1 to 5 do
   for j:=1 to 10 do
     t[i,j]:=random(9);
 p:=0;
 for i:=1 to 5 do
   for j:=1 to 10 do
     if t[i,j] mod 2=0 then p:=p+1;
 if p>25 then writeln('Párosból volt több.')
 else if p=25 then writeln('Ugyanannyi páros és páratlan volt.')
 else writeln('Több páratlan volt.');
 write('Nyomj enter!');
 readln;
END.


25. Írj programot, amely véletlenszámokkal feltölt egy 5x10-es tömböt, majd kiírja az egyes sorokban szereplő számok összegét (röviden, a sorösszegeket)!
megoldás

VAR t:array [1..5,1..10] of integer;
    i,j,s:integer;
BEGIN
 for i:=1 to 5 do
   for j:=1 to 10 do
     t[i,j]:=random(9);
 for j:=1 to 10 do begin
   s:=0;
   for i:=1 to 5 do s:=s+t[i,j];
   writeln(j:2,'. sorösszeg=',s);
 end;
 write('Nyomj entert!'); readln;
END.


26. Írj programot, amely egy 5x10-es tömb "szélső" elemeit 1-re, a többit 0-ra állítja!
megoldás

VAR t:array[1..5,1..10] of integer;
    i,j:integer;
BEGIN
 for i:=1 to 5 do
   for j:=1 to 10 do
     if (i=1) or (i=5) or (j=1) or (j=10)
     then t[i,j]:=1
     else t[i,j]:=0;
END.


27. Írj programot, amely egy 15-elemű tömb elemeit a következőképpen állítja elő: az első két elem értéke 1, a harmadiké 2, a további elemek pedig mindig az őket megelőző három elem összegeként állnak elő. Írasd is ki az elemeket!
megoldás

VAR t:array[1..15] of integer;
    i:integer;
BEGIN
 t[1]:=1;
 t[2]:=1;
 t[3]:=2;
 for i:=4 to 15 do t[i]:=t[i-1]+t[i-2]+t[i-3];
 for i:=1 to 15 do writeln(t[i]);
 write('Nyomj enter!'); readln;
END.


28. Írj programot, amely egy 10-elemű tömböt feltölt 0 és 9 közötti véletlenszámokkal, majd az 5-nél nagyobbakat átrakja egy másik tömbbe!
megoldás

VAR f,g:array[1..10] of integer;
    i,db:integer;
BEGIN
 for i:=1 to 10 do f[i]:=random(10);
 db:=0;
 for i:=1 to 10 do
   if f[i]>5 then begin
     db:=db+1;
     g[db]:=f[i];
   end;
END.


29. Írj programot, amely egy 10-elemű tömböt feltölt 0 és 9 közötti véletlenszámokkal, majd megfordítja a számok sorrendjét!

megoldás

VAR t:array[1..10] of integer;
    i,m:integer;
BEGIN
 for i:=1 to 10 do t[i]:=random(10);
 for i:=1 to 5 do begin
   m:=t[i];
   t[i]:=t[11-i];
   t[11-i]:=m;
 end;
END.

 

 

 

8. Eljárások és függvények

Egy összetettebb programot nehéz megérteni az elejétől a végéig. Ezért fejlesztés során a feladatot több részfeladatra bontjuk, és ezeket egymástól függetlenül készítjük el. Így elegendő csak egy-egy részletre koncentrálnunk. Továbbá ha egy gyakori feladat megoldását elkészítettük, azt a program több pontján is fel tudjuk használni, így időt is megtakarítunk (ez a kód újrahasznosítása).

Az eljárások és függvények a program részben önálló részei. A függvény annyiban több az eljárásnál, hogy visszaad valamilyen értéket (lásd a 3. fejezetet).  Ilyen pl. a sqr(...) függvény, melynek eredménye egy szám (a paraméter négyzete).  A Pascal legtöbb utasítása eljárás: ilyen pl. a writeln(...), mely kiírja a paramétert. Minden eljárás és függvény egy adott feladatot lát el.

Az eljárásokat és függvényeket felhasználásuk előtt deklarálni kell a deklarációs részben, ez a főprogram begin-je elé kerül.

Egyszerű eljárás

Nézzük a következő programot.

BEGIN
  writeln('3+2=');
  write('nyomj entert a megoldáshoz'); readln;
  writeln('5.');
  writeln('5*7=');

  write('nyomj entert a megoldáshoz'); readln;
  writeln('35.');
END.


Látható, hogy a várakozás enterre több helyen is előfordul. Most csináljunk egy eljárást erre a feladatra.

Procedure varj;

  begin

    write('nyomj entert a megoldáshoz');

    readln;

  end;

BEGIN
  writeln('3+2=');
  varj;
  writeln('5.');
  writeln('5*7=');
  varj;
  writeln('35.');

END.

 

A főprogramunk rövidebb és áttekinthetőbb lett. Figyeld meg, hogy az eljárás törzse a főprograméhoz hasonlóan egy összetett utasítás, de nem pont, hanem pontosvessző van a végén. A főprogram az eljárást a nevével indítja, ezt az eljárás hívásának nevezzük.

Paraméteres eljárás

Gondoljunk egy eljárásra (legyen a neve csik), amely - jelekből vízszintes vonalat hoz létre. Az eljárás paramétere, hogy milyen széles legyen a vonal: csik(10) eljáráshívás pl. egy 10 karakter szélességű vonalat húz. Az eljárásban lesz egy for-ciklus. Ennek ciklusváltozóját az eljárás deklarációs részében adjuk meg.

Procedure csik(sz:integer);

  var i:integer;

  begin

     for i:=1 to sz do write('-');

    writeln;

  end;

BEGIN

  csik(10);

  csik(20);

  csik(10);

END.

 

Az eljárás hívásakor a paraméterként átadott szám bekerül az eljárás sz változójába, az első híváskor így az eljárás törzsében sz=10.
Egy eljárásnak több paramétere is lehet, például:
Procedure valami(a,b:integer; x:real); Ilyenkor eljáráshíváskor a megfelelő sorrendben kell megadni az értékeket, ebben a példában valami(1,2,0.11).

Az eljárások általános felépítése tehát:

Procedure eljárásnév(paraméterlista);
   az eljárás saját deklarációs része
   Begin
     az eljárás utasításai {más néven az eljárás törzse}
   End;

Lokális és globális változók

A legutóbbi példaprogramban az i változó az eljárás hívásakor keletkezik, és az eljárás lefutása után megszűnik. Ezért ha a főprogramban hivatkozunk rá, hibaüzenet lesz az eredmény (ismeretlen azonosító). Az ilyen változók neve lokális, mert csak az eljáráson belül léteznek. Lokális minden, az eljárásban deklarált változó, és az eljárás paraméterei is lokális változóként viselkednek

Mi történik, ha a főprogramban is van ilyen nevű változó? Ebben az esetben az eljárás indulásakor a Pascal elmenti a főprogram változóját, az eljáráson belül csak a lokális változó lesz érvényben, majd az eljárás lefutásakor a főprogram visszakapja a saját változóját.

VAR a,b:integer;

Procedure Valami;
   Var a:integer;
   Begin
     a:=5; {lokális változó}
     b:=7; {a fõprogram globális változója}
   End;

BEGIN
   a:=1;
   b:=2;
   Valami;
   Writeln(a,b);
END.

 

Ebben a programban az a és b változókat az eljárás előtt deklaráltuk, ezek tehát az eljáráson belül láthatóak. Viszont az eljárás lokális a változója a főprogram a változója helyébe lép az eljárás futása közben (azt "eltakarja").

A b változó a főprogramé, és az eljárásnak nem lép helyébe lokális változója. Az ilyen változók neve globális: ezek a főprogram eljárásokban is használható változói.

A program által kiírt számok: 1,7. Az eljárás csak a saját lokális a változóját módosítja, a főprogramé változatlan, viszont a b az eljárásban globális.

Mire jó mindez? Az eljárások általában egy önálló részfeladatot oldanak meg. Megírásuk után el is feledkezhetünk arról, hogyan működnek, csak az a fontos, mire valók. Ezért nem jegyezzük meg a bennük használt segédváltozók nevét, és nem baj, hogy máshol is szerepelhetnek ilyen nevű változók.

Összegzésként: az eljárásban általában minden változó legyen lokális, kivéve, ha a feladat kifejezetten a főprogram egyes változóinak módosítása.

Függvények

A függvény majdnem olyan, mint egy eljárás, csak rendelkezik visszatérési értékkel. Az eljáráshoz képest meg kell adnunk a visszatérési érték típusát, és a függvény törzsében valahol magát az értéket.

A következő példa elkészíti a köbre emelő függvényt, az sqr mintájára. Ennek a függvénynek egy paramétere van (amit köbre emelünk).

Function kob(a:real):real;
   Begin
     kob:=a*a*a;
   End;

BEGIN
   writeln(kob(65));
END;

 

A függvény törzsében látható értékadás nem értékadás valójában, hanem a visszatérési értéket adja meg. Tehát:

Function függvénynév(paraméterlista):típus;
   a függvény saját deklarációs része
   Begin
     ...

     függvénynév:=visszatérési érték;
     ...

   End;

 

A következő példa az egész számok hatványozását mutatja be egy kétparaméteres függvénnyel (a függvénynek akárhány paramétere lehet, de visszatérési értéke csak egy!). A hatványozást többszöri szorzással végzi el.

Function hatv(x,n:integer):integer;

  var i,p:integer;

  begin

    p:=1;

    for i:=1 to n do p:=p*x;

    hatv:=x;

  end;

BEGIN

  if hatv(2,3)=8 then writeln('Működik!');

END.

 

Felmerülhet a kérdés, hogy mi szükség volt a p változóra, miért nem használtuk hatv-ot. Vigyázat, hatv nem változó! A hatv:=hatv*x jobb oldala függvényhívás, itt a függvény meghívhatja saját magát.

Változóparaméterek

Keressük meg a hibát a következő programban, amely két változó értékét felcserélő eljárást használna!

Procedure Csere(a,b:integer);
  Var m:integer;
  Begin
    m:=a; a:=b; b:=m;
  End;

VAR p,q:integer;

BEGIN

  p:=8;
  q:=4;
  Csere(p,q);
  Writeln(p,q);
END.

 

A program a 8,4 számokat fogja kiírni, hiszen az eljárás a főprogram eredeti p,q változóját nem módosította, csak a lokális a,b értékét cserélte fel. A Pascal ilyen esetben érték szerinti paraméterátadást használ, vagyis az a,b változók értéke az átadott két paraméter (szám) lesz.

Lehet azonban cím szerinti paraméterátadást használni. Cseréljük ki a program első sorát erre, és működni fog:

Procedure Csere(var a,b:integer);

 

A különbség a paraméterlistát megelőző var. Ebben az esetben eljáráshíváskor nem használhatunk értékeket, csak változókat. Az eljárásban létrejövő paraméterek ugyanazokra a memóriacímekre fognak mutatni, mint a hívásban szereplő változók. Vagyis ami a,b változókkal történik az eljárásban, az történik a főprogram p,q változóival. (Használhatunk vegyesen "sima" és változóparamétereket is.)

Így működik a Pascal readln(változó) utasítása is, az átadott változó értékét módosítja.

Feladatok

30. Készíts függvényt, mely a két átadott egész szám közül a nagyobbikat adja vissza! Vagyis a következő programrészlet elé írva az jól fog működni.

...
BEGIN

  writeln(max(2,7)); //7-et ír ki

END.

megoldás

Function max(a,b:integer):integer;
  Begin
    if a>b then max:=a
    else max:=b;
  End;

 

31. Egészítsd ki a következő programrészletet a megfelelő eljárással, amely az adott karaktert adott számszor írja ki!

...

BEGIN

  write('B');  //Brrrrrrrrrrrr! Nagyon hiiiiideg van!

  ism('r',12);

  write('! Nagyon h');

  ism('i',5);

  writeln('deg van!');

END.

megoldás

Procedure ism(c:char;n:integer);
  Var i:integer;
  Begin
    for i:=1 to n do write(c);
  End; 

 

32. Most készíts függvényt ugyanerre a célra! A feladata előállítani az adott karaktert adott számszor tartalmazó stringet. Segítségként megadom a függvény elejét is.

Function ism(c:char;n:integer):string;
...

BEGIN

  write('B',ism('r',12),'! Nagyon h',ism('i',5),'deg van!');

END.

megoldás

Function ism(c:char;n:integer):string;
  Var s:string;
      i:integer;
  Begin
    s:='';
    for i:=1 to n do s:=s+c;
    ism:=s;
  End;

 

33. Készíts eljárást, mely a paraméterként átadott egész típusú változót a következő páros számra kerekíti!

...
VAR a:integer;

BEGIN

  a:=53;

  parit(a);

  writeln(a); //a értéke 54 lesz

  parit(a);

  writeln(a); //a értéke marad 54

END.

  megoldás

Procedure parit(var x:integer);
  Begin
    if x mod 2=1 then x:=x+1;
  End; 

 

 

 

9. Szövegkezelés és példák

Ebben a fejezetben megmutatom, hogyan lehet a szöveget karakterenként kezelni. Ezen kívül, a korábban tanultak felhasználásával, már összetettebb feladatokkal is találkozhatsz, melyek általános programozási technikákat mutatnak be. A továbbiakban az ilyen példák egyre gyakoribbak lesznek, tehát akkor is érdemes egy fejezetet elolvasnod, ha éppen az a téma (most a szövegkezelés) nem érdekel.

A szöveg karakterei

Egy string típusú változó úgy viselkedik, mint egy karakterekből álló tömb. Így s[2] a string második karakterét jelenti. Ha minden karaktert fel szeretnénk dolgozni, szükségünk lesz a string hosszára: ezt a length függvény adja meg.

A következő példa kiírja a szöveget függőlegesen (soronként egy karaktert):

Var s:string;
    i:integer;

BEGIN
  write('Írj be valamit: '); readln(s);
  for i:=1 to length(s) do writeln(s[i]);
  write('nyomj entert'); readln;
END. 

 

Figyelem! Ezek a példák ékezetes karakterekkel csak úgy fognak működni, ha a fájl kódolását CP852-re állítottad! (2. fejezet). Ennek okáról és az utf-8 kódolásról később, a grafikus felület kezelésénél lesz szó.

Kidolgozott példa: magánhangzóvizsgálat

Számoljuk meg egy string magánhangzóit! A ciklusunk ilyesféle feltételt tartalmazna:

if (s[i]='a') or (s[i]='A') or (s[i]='á') or (s[i]='Á')...

ez így, a kis- és nagybetűket figyelembe véve, nagyon hosszadalmas lenne. Ha azonban a magánhangzók listája szerepelne egy változóban, csak azt kellene megvizsgálni, hogy az adott karakter szerepel-e ebben.


m:='öüóeuioőúaéáűíÖÜÓEUIOŐÚAÉÁŰÍ'

Hogyan állapítsuk meg, hogy a szövegünk s[i] karaktere szerepel-e m-ben? Egy ciklussal végiglépkedünk m összes karakterén, amíg nem találunk egyezést s[i]-vel, vagy m végére nem érünk. Ez most nem for-ciklus lesz, mert nem tudjuk előre, meddig kell számolnia. Az elöltesztelős ciklusnak viszont a végrehajtás feltételét kell megadni, ami az, hogy nem találtunk egyezést és nem értünk a végére.

j:=1;

while (j<=length(m)) and (s[i]<>m[j]) do j:=j+1;

A ciklus kétféleképpen állhat le. Ha a második feltétel nem teljesül, akkor s[i] szerepel m-ben a j. helyen, vagyis magánhangzó. Ha az első feltétel nem teljesül, akkor j>length(m), vagyis végigmentünk m-en, de nem találtunk egyezést

 

Ha végig nincs egyezés, akkor az utolsó lépésben j=length(m), és s[i]<>m[j], tehát j nő eggyel. A következő vizsgálatnál az első részfeltétel hamis lesz, hiszen j>length(m). Mi a helyzet a második feltétellel? Az értelmetlen, mert m-nek nincs annyiadik karaktere. Nem fog ez hibát okozni a programban?

Nem. A Pascal (az alapbeállítások használata esetén) az AND-del összekapcsolt feltételeket úgy vizsgálja, hogy ha az első feltétel hamis, a másodikat már nem ellenőrzi (mert az eredmény úgyis hamis). Hasonlóképpen, az OR-ral összekapcsolt két feltétel közül ha az első igaz, a másodikat már nem vizsgálja. Ezért nem mindegy, hogy a feltételeket milyen sorrendben írjuk be.

 

Végül a magánhangzóvizsgálatot betesszük egy mgh nevű függvénybe. A függvényérték típusa most boolean lesz (igaz, ha a karakter magánhangzó).

Var s:string;
    i,c:integer;

Function mgh(c:char):boolean;
  var m:string;
      j:integer;
  begin
    m:='öüóeuioőúaéáűíÖÜÓEUIOŐÚAÉÁŰÍ';
    j:=1;
    while (j<=length(m)) and (c<>m[j]) do j:=j+1;
    mgh:=(j<=length(m));
  end;

BEGIN
  write('Írj be valamit: '); readln(s);
  c:=0;
  for i:=1 to length(s) do
    if mgh(s[i]) then c:=c+1;
  writeln(c,' magánhangzó volt benne.');
  write('nyomj entert'); readln;
END.       

 

Magyarázat: az eljárásnak a vizsgálandó karaktert paraméterként adjuk át, ez c lesz, nem keverendő a főprogram számláló c-jével. mgh igazságértéke abból derül ki, hogy j nem lépte túl m hosszát.

További hasznos szövegkezelő lehetőségek

A Pascal sok szövegkezelő függvényt és eljárást ismer, amelyekkel az egyes karakterek vizsgálatánál gyorsabban megoldhatjuk a feladatokat.

Pos(string1,string2) kereső függvény megadja, hogy string1 hányadik karakternél kezdődik string2-ben. 0 lesz a végeredmény, ha nem szerepel benne. Az első előfordulás számít, tehát pos('ép','szép kép') eredménye 3 lesz.

Így az előző program függvénye egyszerűbben is megoldható, ciklus nélkül:

mgh:=(pos(c,m)>0)

 

Copy(string,szám,darab) függvény eredménye olyan részszöveg, mely string szám-adik karakterétől kezdődik, és darab hosszú. Vagyis copy('malac',3,2) eredménye a 'la' string.

Delete(string,szám,darab) eljárás, mely módosítja string változót (változóparaméter!) úgy, hogy töröl belőle a szám-adiktól kezdve darab karaktert.

Insert(mit,mibe,szám) eljárás, mely mibe string típusú változóba szám-adik karakterétől beszúrja mit szöveget.

Ne feledkezzünk meg a stringek közötti + műveletről, mely az összefűzésüket jelenti.

Ez a példa kitörli S stringből az összes kettőspontot:

while pos(':',S)>0 do delete(S,pos(':',S),1);

 

Ez pedig, ha NEV egy "vezetéknév szóköz keresztnév" formátumú string, K-ba beteszi a keresztnevet.

K:=copy(NEV,pos(' ',NEV)+1,length(NEV)-pos(' ',NEV));

Feladatok

34. Írj programot, amely a beírt szövegben megszámolja az "e" betűket!
megoldás

Var c,i:integer;
    s:string;

BEGIN
  write('Írj be valamit: '); readln(s);
  c:=0;
  for i:=1 to length(s) do
    if s[i]='e' then c:=c+1;
  writeln(c,' db. e betű volt.');
  write('nyomj entert'); readln;
END.


35. írj programot, amely a B változóba beteszi az A változóban tárolt szöveg megfordítását!

megoldás

Var i:integer;
    A,B:string;

BEGIN
  A:='próbaszöveg';
  B:='';
  for i:=length(A) downto 1 do
    B:=B+A[i];
END.  

 

36. Írj programot, amely a beírt szöveget szavanként írja ki!

megoldás

Var i:integer;
    s:string;

BEGIN
  write('Írj be valamit: '); readln(s);
  for i:=1 to length(s) do
    if s[i]=' ' then writeln
    else write(s[i]);
  writeln;
  write('nyomj entert'); readln;
END.  

 

37. Írj programot, amely a megadott stringből törli a fölösleges szóközöket (ahol egymás mellett több van)!

megoldás

Var s:string;

BEGIN
  write('Írj be valamit: '); readln(s);
  //minden dupla szóközből egyet törlünk
  while pos('  ',s)>0 do
    delete(s,pos('  ',s),1);
  writeln(s);
  write('nyomj entert'); readln;
END

 

38. Írj programot, amely a beírt szövegből csak a mássalhangzókat írja ki! Használd benne a fenti mgh függvényt.

megoldás

Function mgh(c:char):boolean;
  var m:string;
  begin
    m:='öüóeuioőúaéáűíÖÜÓEUIOŐÚAÉÁŰÍ';
    mgh:=(pos(c,m)>0);
  end;

Var s:string;
    i:integer;

BEGIN
  write('Írj be valamit: '); readln(s);
  for i:=1 to length(s) do
    if not mgh(s[i]) then write(s[i]);
  writeln;
  write('nyomj entert'); readln;
END.

 

39. Írj programot, amely a beírt szöveget "madárnyelven" írja ki! (pl. tulipán->tuvulivipáván)

megoldás

Function mgh(c:char):boolean;
  var m:string;
  begin
    m:='öüóeuioőúaéáűíÖÜÓEUIOŐÚAÉÁŰÍ';
    mgh:=(pos(c,m)>0);
  end;

Var s:string;
    i:integer;

BEGIN
  write('Írj be valamit: '); readln(s);
  for i:=1 to length(s) do begin
    write(s[i]);
    if mgh(s[i]) then write('v',s[i]);
  end;
  writeln;
  write('nyomj entert'); readln;
END. 

 

40. Készíts függvényt, amelynek eredménye a megadott szöveg, az összes "j"-t "ly"-ra cserélve!

megoldás

Function jtoly(s:string):string;
  var x:string;
      i:integer;
  begin
    x:='';
    for i:=1 to length(s) do
      if s[i]='j' then x:=x+'ly'
      else x:=x+s[i];
    jtoly:=x;
  end;

BEGIN
  writeln(jtoly('jó jégpálya'));
  write('nyomj entert'); readln;
END.

 

 

10. Fájlkezelés

Bár sok programnak megvan a maga bináris fájlformátuma, mellyel az alább leírtaknál hatékonyabban tud dolgozni, ebben a fejezetben csak azt a fájlformátumot vesszük, amelyet minden program ugyanúgy kezel. Ez a szöveges fájl. A szöveges fájl csak karaktereket tartalmaz, és sorokra van tagolva. Az ékezetes karaktereket azonban egy szöveges fájlban is többféleképpen lehet tárolni.

A szöveges fájl műveletei: új fájl létrehozása, fájl elejére állás, következő adat beolvasása, adat írása a szöveges fájl végére. Emiatt az adatokat csak sorban tudjuk beolvasni, az utolsó sort csak akkor, ha már beolvastuk az összes előtte lévőt.

A fájlkezelés utasításai

Fájl deklarálása: létre kell hoznunk egy text típusú változót, az összes fájlműveletben ezt fogjuk használni.

Ezután hozzá kell rendelni a változóhoz egy adott fájlt annak útvonala segítségével: assignfile(fájl,útvonal)

Megnyitás olvasásra (a fájlt vagy írásra, vagy olvasásra nyitjuk meg): reset(fájl)

Megnyitás írásra (ez új fájlt hoz létre, ha van már ilyen, azt törli): rewrite(fájl)

Megnyitás írásra létező fájlnál úgy, hogy a végéhez fűzünk adatot: append(fájl)

Fájl törlése: erase(fájl)

Olvasás: read/readln(fájl,változók)

Írás: write/writeln(fájl,kifejezések)

Vége van-e a fájlnak (beolvastuk-e az utolsó adatot, függvény, igaz/hamis): eof(fájl)

Vége van-e egy sor beolvasásának (függvény): eoln(fájl)

Fájl lezárása (erre főként írásnál van szükség, hogy az utolsó adatok is bekerüljenek a fájlba): closefile(fájl)

A read és write utasításokat úgyanúgy kell használni, mint amikor a billentyűzetről olvasunk és a képernyőre írunk, csak most megadjuk a fájlt is.

Példák olvasásra és írásra

A következő példákhoz már fájlra is szükség lesz. Szöveges fájlt legegyszerűbben a Windows Jegyzettömb alkalmazásával készíthetünk, de bármilyen szövegszerkesztő megfelel, ha a Mentés másként, szöveges fájl (txt) lehetőséget használod.

Most a fájlt is egy programmal hozzuk létre. A fájlok alapértelmezett helye (relatív útvonal megadásával, pl. 'proba1.txt') a projekt mappája, de teljes útvonal megadásával (pl. 'c:\programozas\fajl\proba.txt') más mappát is használhatunk.

A fájlnevekben szándékosan nem használok ékezetet. Ennek oka, hogy a Lazarus alapértelmezetten utf-8 kódolást használ a szerkesztőben. Ezt átállítottuk cp852-re a konzolablakban megjelenő karakterek miatt. A fájlrendszer azonban egy harmadik fajta kódolást használ (cp1250/ANSI).  E kódlapok között lehet megfelelő függvényekkel konvertálni, de egyszerűbb, ha most mellőzzük az ékezeteket.


A fájl előállítása:

VAR f:text;
    i:integer;
BEGIN
  assignfile(f,'proba1.txt');
  rewrite(f);
  for i:=1 to 10 do
    writeln(f,i,' ',random(100));
  closefile(f);
END.

 

A most elkészült fájl soronként két számot tartalmaz, szóközzel elválsztva:
1 79
2 13
...
10 7

 Ha egy sorban több szám van szóközzel elválasztva, ezeket beolvashatjuk így:
readln(f,a,b).
Vagy:
read(f,a);  readln(f,b);

Vagy:

read(f,a); read(f,b); readln(f);

Az adatok beolvasásánál ha tudjuk előre, hány sor következik, használhatunk for-ciklust. Ha nem, vizsgálnunk kell, mikor van vége a fájlnak. A következő program kiírja a sorok második számainak összegét:

VAR f:text;
    s,a,b:integer;
BEGIN
  assignfile(f,'proba1.txt');
  reset(f);
  s:=0;
  while not eof(f) do begin
    readln(f,a,b);
    s:=s+b;
  end;
  closefile(f);
  writeln(s);
  write('enter:'); readln;
END.

 

Látható, hogy a második számokhoz úgy jutunk el, hogy az elsőket is beolvassuk.

Végül nézzük, hogy lehet egy fájlból egy másikat előállítani. Ez a program leszedi minden sorból az első számot, és csak a másodikat írja fájlba:

VAR f,g:text;
    a,b:integer;
BEGIN
  assignfile(f,'proba1.txt');
  assignfile(g,'proba2.txt');
  reset(f);
  rewrite(g);
  while not eof(f) do begin
    readln(f,a,b);
    writeln(g,b);
  end;
  closefile(g);
  closefile(f);
END.

Szöveg a fájlban

Ha a fájl egy sorából szöveget szeretnénk beolvasni a readln(f,s) utasítással (s string típusú), az egy teljes sort beolvas. Ha a sor szóközöket tartalmaz, akkor azok is bekerülnek a stringbe. Ekkor nem tudjuk a szóközzel elválasztott szavakat külön változóba tenni. A beolvasott string szavakká darabolásával a következő fejezetben foglalkozunk.

Kivétel, ha a sor számokat és szöveget tartalmaz, és a számok a szöveg előtt vannak. Ekkor a szóközzel elválasztott számokat még külön be tudjuk olvasni. Ha pl. a sor tartalma:

3 2 1 rajt cél

akkor a readln(f,a,b,c,s) utasítás beolvassa külön a 3 számot, de az utána következő két szót már egyben, s-be.

Ha előbb van a szöveg, és utána a számok, akkor egyetlen stringbe tudjuk csak őket beolvasni. Ilyenkor a pos, copy függvényekkel megkereshetjük a szóközt, és kivághatjuk a szóközök közötti részt. Vagy használhatjuk a Free Pascal fejlett szövegdaraboló eljárásait, a következő fejezetből.

Feladatok

A következő feladatok bemeneti fájljait előállíthatod a Jegyzettömbbel is, de mellékeltem mintafájlokat, amelyeket az oldal aljáról letölthetsz. Ne felejtsd ezeket a projekt mappájába másolni!

feladat1.txt; feladat2.txt; feladat3.txt

41. Írj programot, a mely megszámolja, hány szöveges sor van egy fájlban (feladat1.txt)! Tipp: minden sort be kell olvasni.

megoldás

VAR f:text;
    c:integer;
BEGIN
  c:=0;
  assignfile(f,'feladat1.txt');
  reset(f);
  while not eof(f) do begin
    readln(f); //megspóroltunk egy változót, de lehetett volna readln(f,s) is
    c:=c+1;
  end;
  closefile(f);
  writeln(c);
  write('enter:'); readln;
END.

 

42. Számold meg a fájlban (feladat1.txt) lévő e-betűket! Tipp: olvasd be a sorokat, majd minden sorban számold meg az e-ket.

megoldás

VAR f:text;
    c,i:integer;
    s:string;
BEGIN
  c:=0;
  assignfile(f,'feladat1.txt');
  reset(f);
  while not eof(f) do begin
    readln(f,s);
    for i:=1 to length(s) do
      if (s[i]='e') or (s[i]='E') then c:=c+1;
  end;
  closefile(f);
  writeln(c);
  write('enter:'); readln;
END.

 

43. Készíts kimeneti fájlt, mely tartalmazza az eredeti fájlban (feladat1.txt) a sor számát és azt, hogy hány karakter hosszú!

megoldás

VAR f,g:text;
    s:string;
    c:integer;
BEGIN
  assignfile(f,'feladat1.txt');
  assignfile(g,'megoldas.txt');
  reset(f);
  rewrite(g);
  c:=0;
  while not eof(f) do begin
    readln(f,s);
    c:=c+1;
    writeln(g,c,': ',length(s));
  end;
  closefile(g);
  closefile(f);
END.

 

44. Írj programot, mely egy egész számokat tartalmazó fájlt (feladat2.txt) fordított sorrendben kiír a képernyőre! A fájlban soronként egy szám van, legfeljebb 100 db. Ehhez a számokat el kell tárolnod egy tömbben.

megoldás

VAR f:text;
    i,db:integer;
    t:array[1..100] of integer;
BEGIN
  assignfile(f,'feladat2.txt');
  reset(f);
  db:=0;
  while not eof(f) do begin
    db:=db+1;
    readln(f,t[db]);
  end;
  closefile(f);
  for i:=db downto 1 do
    writeln(t[i]);
  write('enter:'); readln;
END.

 

45. Egy fájl (feladat3.txt) első sora egy számot tartalmaz, az utána következő sorok számát. A többi sorban két-két szám van szóközzel elválasztva. A program írja ki a számpárok szorzatának összegét!

megoldás

VAR f:text;
    db,i,a,b,s:integer;
BEGIN
  assignfile(f,'feladat3.txt');
  reset(f);
  readln(f,db);
  s:=0;
  for i:=1 to db do begin
    readln(f,a,b);
    s:=s+a*b;
  end;
  closefile(f);
  writeln(s);
  write('enter:'); readln;
END

 

 

11. Érettségi túlélőcsomag 1 (szövegdarabolás, konvertálás)

Az előző fejezetekből már majdnem minden eszközt megismertél, ami egy sikeres emelt szintű érettségi programozási feladatának megoldásához kell. Ebben a fejezetben megmutatom, hogy lehet egy szöveges fájlból mindenféle adatot kinyerni, és megismerkedhetsz a moduláris programozás alapegységével, a unittal.

Unitok

Egy komolyabb projekt már túl nagy ahhoz, hogy egyetlen forrásfájl tartalmazza. Ekkor a program egyes részeit (jellemzően: a különböző részfeladatokat) külön fájlokban, modulokban készítjük el. A modulok külön fordíthatóak, végül ezeket összeépítjük a főprogrammal. Ennek a fejlesztési módszernek több előnye van. Lehetővé teszi, hogy a projektet többen fejlesszék, mindenki csak a saját részfeladatára összpontosítva. Mivel a modulok külön fordíthatóak, le is lehet őket tesztelni, és az utolsó lépésben a modulok működését nem, csak az együttműködésüket kell ellenőrizni. Ezzel a tesztelés gyakran nagyon hosszú feladatát is szétoszthatjuk.

A modul neve Pascalban unit. A unitok többek között eljárások, függvények deklarációját tartalmazzák. A Free Pascal rendszer elemeit is unitokra darabolva készítették el, minél több lehetőségét használjuk fel a nyelvnek, annál több unitot építünk hozzá a főprogramunkhoz. A legfontosabb unit neve system, ennek használata a programunkban automatikus. Ez a unit tartalmazza pl. a write eljárás deklarációját is. Ha azonban további unitok eljárásait is használjuk a programban, azokat deklarálnunk kell. Ez a programunk legelső deklarációja (rögtön a program sor után):

USES unitok listája;

A következő példa a konzolképernyő törlését mutatja be.

USES Crt;

BEGIN

  writeln('Ez nem látszik.');

  clrscr;

  writeln('Ez igen.');

  readln;

END.

 

A képernyőt a clrscr utasítás törli, amelyet a crt unitban deklarálnak. A crt unit megadása nélkül a fordító ismeretlen azonosító hibajelzést ad. Az összes unit összes eljárásának leírása megtalálható a www.freepascal.org/docs-html/rtl címen.

Szövegdarabolás

A bemenő adatokat gyakran szöveges fájlban kapjuk meg, egy sorban több adattal, közöttük valamilyen elválasztó karakterrel. Ha az adatok között szöveg is van, fel kell darabolnunk a bemeneti sort az elválasztó karakterek mentén. Ehhez az strutils unit függvényeit használjuk fel, bár írhatnánk rá saját programot is, de az tovább tartana.

USES Strutils;

VAR s:string;

BEGIN

  s:='aaa:bbb::ccc';

  writeln(wordcount(s,[':'])); //3

  writeln(extractword(3,s,[':'])); //ccc

  writeln(extractdelimited(3,s,[':'])); //üres

  readln;

END.

 

A programban felbukkan egy új típus: a halmaz. A [':'] egy set of char típusú adat, amely karakterhalmazt jelent, ebben a programban az elválasztó karakterek halmazát. Lehetett volna [':',' ',';'] is. Elválasztó karakter természetesen az adatokon belül nem lehet.

Az egymás utáni több elválasztót kétféleképpen is lehet értelmezni. Számíthatnak egynek, ekkor a 'bbb' után 'ccc' következik. De az is lehet, hogy a két ':' között egy üres szöveg szerepel adatként. Az első értelmezésben üres szöveg nem lehet adat.

wordcount(string,elválasztók halmaza) függvény megadja a stringben a darabok számát. Az extractword(sorszám,string,elválasztók halmaza) megadja az adott sorszámú rész-stringet. Ennél a két függvénynél egymás utáni elválasztók egynek számítanak.

Az extractdelimited(sorszám,string,elválasztók halmaza) az előző függvényhez hasonló, de itt üres string is lehet adat, ezért a fenti példában a harmadik darab a két ':' közötti üres string.

Típuskonverzió

Mi a teendő, ha a beolvasott sor szöveges és számadatokat is tartalmaz?

USES Strutils;
VAR s:string;
     n:integer;
BEGIN
  s:='aaa:12';
  n:=extractdelimited(2,s,[':']);
END.

 

A kód fordítási hibát eredményez. Ennek oka, hogy a daraboló függvény végeredménye string típusú, és  a Pascal nem értelmezi számként. Ilyen esetben típuskonverziós függvényeket használunk. Ezek a függvények a sysutils unitban vannak. A fenti program javítva:

USES Strutils,Sysutils;
VAR s:string;
     n:integer;
BEGIN
  s:='aaa:12';
  n:=strtoint(extractdelimited(2,s,[':']));
END.

 

Az strtoint(szöveg) függvény eredménye integer típusú, a szöveg  egésszé konvertálva. Hasonlóképpen működik az strtofloat, amelynek eredménye lebegőpontos (real) típusú. Fontos viszont, hogy a

writeln(strtofloat('12.33'));

hibás, mert a függvény tizedesvesszőnek nem a Pascal '.' karakterét, hanem az operációs rendszerben beállított karaktert használja, ami magyar Windows esetén ','. A fenti sor helyesen:

writeln(strtofloat('12,33'));


Így viszont egy adott programkód helyes működése az operációs rendszer beállításaitól függ, ami nem szerencsés. Továbbá, ha a bemeneti fájlunkban '.' a tizedespont, nem tudjuk feldolgozni. A tizedes karaktert a
sysutils által beállított decimalseparator változó tartalmazza, amit a programunkban átállíthatunk. Helyes tehát a következő kódrészlet is:

decimalseparator:='.';
writeln(strtofloat('12.33'));


Az
inttostr és floattostr függvények számot alakítanak át stringgé. A következő sor:

writeln(floattostr(12.33));

a várttal ellentétben 12,33-at ír ki, mert a floattostr is a decimalseparator alapján működik.

Mi történik, ha a beolvasott szöveg nem tartalmaz érvényes számot? Ilyen esetben az
strtoint hibajelzéssel leállítja a programot. Használhatjuk ilyenkor a trystrtoint(szöveg,változó) függvényt, mely a szöveget egésszé konvertálja és a megadott integer típusú változóba helyezi (ld. cím szerinti paraméterátadás), kimeneti értékként pedig visszaadja, sikeres volt-e a konverzió (vagyis a függvény típusa boolean).  A példaprogram a beadott számokat összegzi, *-ra kilép. Ezért a beolvasás csak string típusú változóba mehet. A trystrtoint szerepe a példában kettős: egyrészt megadja, sikeres-e a konverzió, másrészt (mellékhatásként) el is végzi a konverziót. Mellékhatással rendelkező függvények használata a programkódot nehezebben érthetővé, ugyanakkor tömörebbé teszi.

USES Sysutils;
VAR s:string;
    n,x:integer;

BEGIN
  x:=0;
  repeat
    write('Szám (*=kilépés): ');
    readln(s);
    if trystrtoint(s,n) then x:=x+n;
  until s='*';
  writeln(x);
  readln;
END.

      
A Free Pascal unitjaiban rengeteg hasznos segédeszközt találhatunk. Ezeket egy adott probléma megoldása közben érdemes megkeresni, akár keresővel (pl. "free pascal convert date to string" keresőkifejezés), akár a dokumentációt böngészve.

Feladatok

feladat4.txt; feladat5.txt

46. Egy szöveges fájlban (feladat4.txt) nevek szerepelnek keresztnév szóköz vezetéknév formában soronként. A program írja ki képernyőre a neveket vezetéknév vessző keresztnév formátumban!

megoldás

USES Strutils;
VAR s:string;
    f:text;
BEGIN
  assignfile(f,'feladat4.txt');
  reset(f);
  while not eof(f) do begin
    readln(f,s);
    writeln(extractword(2,s,[' ']),', ',extractword(1,s,[' ']));
  end;
  closefile(f);
  readln;
END.

 

47. Egy szöveges fájlban (feladat5.txt) állatnevek és az állatok maximális sebessége szerepel állatnév kettőspont szóköz sebesség formában, a sebesség tizedesponttal van megadva m/s-ban. A program írja ki az állatokat és a hozzájuk tartozó sebességet km/h-ban!

megoldás

USES Strutils,Sysutils;
VAR s:string;
    f:text;
BEGIN
  decimalseparator:='.';
  assignfile(f,'feladat5.txt');
  reset(f);
  while not eof(f) do begin
    readln(f,s);
    write(extractword(1,s,[' ',':']),': ');
    writeln(strtofloat(extractword(2,s,[' ',':']))*3.6:0:2);
  end;
  closefile(f);
  readln;
END.

 

 

12. Érettségi túlélőcsomag 2 (gyakori algoritmusok)

Ebben a fejezetben néhány gyakran előforduló algoritmust ismertetek. Ezekről sokkal bővebben olvashatsz itt. Érdemes lenne őket alaposan elemezni, de most csak az a célom, hogy az érettségi feladatok megoldásához rendelkezésedre álljon a minimális eszközkészlet. Ez természetesen nem elég: sok gyakorlásra van szükség ahhoz, hogy kialakítsd a feladatok megoldásához szükséges gondolkodásmódot, és az egyszerűbb feladatokat gyorsan, rutinosan oldd meg. Ehhez egy ilyen tananyag kevés: szakkörre, tanórára van hozzá szükség, de jó gyakorlás az érettségi feladatok megoldása, mintamegoldásának elemzése is.

A feladatok tömbökről szólnak. Ez többnyire elég, hiszen fájlból is be lehet olvasni az adatokat egy vagy több tömbbe. Ehhez szükséges, hogy ismerjük az adatok maximális mennyiségét, mert akkora tömböket kell deklarálnunk.

Ebben a fejezetben minden példa ugyanazokról az adatokról szól. Legyen egy szöveges fájl, amely azt tartalmazza, hogy egyes színeket mennyire tartanak vidámnak, 1..10-es skálán.

sárga 10

fekete 1

világoszöld 9

sötétzöld 3
...

A fájlban legfeljebb 100 sor lehet. Ekkor az adatokat beolvashatjuk két tömbbe: szin és vidam, a következőképpen:

db:=0;

while not eof(f) do begin

  readln(f,s);

  db:=db+1;

  szin[db]:=extractword(1,s,[' ']);

  vidam[db]:=strtoint(extractword(2,s,[' ']));

end;

Keresés

Keressük meg, mennyire vidám a piros szín! Lehet, hogy nincs piros az adatok között, erre is készüljünk fel! A 9. fejezetben láthattál ehhez hasonló példát.

i:=1;

while (i<=db) and (szin[i]<>'piros') do i:=i+1;

if i<=db then writeln('Ilyen vidám a piros: ',vidam[i])

else writeln('nincs piros');


Emlékezz arra, hogy ha a
while első feltétele hamis, vagyis i>db, akkor a másodikat már nem vizsgálja. i tehát vagy az első pirosnál áll meg, vagy db+1 értéket vesz fel.

Maximumkiválasztás

Keressük meg a legvidámabb színt! A ciklus a legvidámabb szín sorszámát fogja megadni. Kezdjük az első színnel, majd minden további színnél megnézzük, vidámabb-e az eddigi legvidámabbnál, és ha igen, az lesz a legvidámabb.

max:=1;

for i:=2 to db do if vidam[i]>vidam[max] then max:=i;

writeln('Legvidámabb: ',szin[max]);

writeln('Ilyen vidám: ',vidam[max]);

 

Fontos, hogy max nem a vidámságot, hanem annak sorszámát adja meg! Ez jól is jön, mert így a megfelelő színt is meg tudjuk adni. A program most a legelső legvidámabb színt adta meg, ha a > jelet >=-re cseréljük, akkor a legutolsót kapjuk. A program könnyen átalakítható minimumkiválasztásra.

Rendezés

A rendezés nagyon hasznos algoritmus. Egyrészt gyorsítja a keresést (a szótárban azért tudunk gyorsan keresni, mert a szavak ábécérendben vannak, ha össze-vissza lennének, egy szó miatt a teljes szótárat végig kellene néznünk), másrészt rendezés után az egyformák egymás mellé kerülnek, így pl. könnyen csoportosíthatjuk a színeket vidámság szerint. Mivel a példáink kevés adatot tartalmaznak, a lassú keresés (soros keresés) is gyorsan lefut, ezért inkább a második ok miatt rendezünk.


Az itt bemutatott maximum- (minimum-) kiválasztásos rendezés lényege, hogy pl. csökkenő sorrendhez először megkeressük a legnagyobbat, és kicseréljük a legelső elemmel. Mivel most már a legelső elem a helyén van, ugyanezt a lépést a második elemtől kezdve ismételjük meg. A második helyre tehát bekerült a második legnagyobb. Végül az utolsó előtti és az utolsó elemre hajtjuk végre a maximumkiválasztást.

ciklus i:=1..db-1

max:=a maximális elem sorszáma i és db között

az i-edik és max-adik elem kicserélése

ciklus vége

Ez az algoritmus csökkenő sorrendbe tesz. Minimumkiválasztással növekvő sorrendet kapunk.

A példabeli adatoknál ügyelni kell arra, hogy ha két vidámságot felcserélünk, a hozzájuk tartozó két színt is fel kell cserélni! Rendezzük hát a színeket vidámság szerint csökkenő sorrendbe. (Az utolsó ciklus nem tartozik a rendezéshez.)

for i:=1 to db-1 do begin

//maximumkiválasztás:
  max:=i;

  for j:=i+1 to db do if vidam[j]>vidam[max] then max:=j;

//csere:
  mv:=vidam[i]; vidam[i]:=vidam[max]; vidam[max]:=mv;

  ms:=szin[i]; szin[i]:=szin[max]; szin[max]:=ms;

end;
for i:=1 to db do writeln(szin[i],' ',vidam[i]);

 

A többszempontú rendezés azt jelenti, hogy ha az első szempont szerint több egyforma van, akkor azokat még egy másik szempont szerint sorba rakja. A példában az egyforma vidám színek szomszédosak lesznek a rendezés után, ezeket rendezhetjük a szín neve szerint ábécébe. A relációs jelek szövegek között is működnek, az a string "nagyobb", amelyik ábécérendben később következik. Ez sajnos csak az angol ábécé karaktereire működik, ráadásul megkülönbözteti a kis- és nagybetűket, de példának jó lesz.

A kulcs a maximumkiválasztás relációjának módosítása, mert ez határozza meg, hogy egy adatot mikor kell előbbre hozni a tömbben. Az eredeti feltétel: vidam[j]>vidam[max], ha ez teljesül akkor kell a j. adatot előbbre hozni. Ezt módosítjuk:
(vidam[j]>vidam[max]) or ((vidam[j]=vidam[max]) and (szin[j]<szin[max]))

Tehát ha vidámabb a szín, előbbre jön, ha két szín egyforma vidám, akkor az ábécében korábbi jön előre.

Feladatok

feladat6.txt

48. Olvasd be két tömbbe a példákban szereplő fájlt (feladat6.txt), majd írd ki a legkevésbé vidám színeket! (Tipp: minimumkiválasztás, majd a minimálissal egyenlők kiírása.)

megoldás

USES Sysutils,Strutils;
VAR f:text;
    i,db,min:integer;
    s:string;
    szin:array[1..100] of string;
    vidam:array[1..100] of integer;
BEGIN
  assign(f,'feladat6.txt');
  reset(f);
  db:=0;
  while not eof(f) do begin
    readln(f,s);
    db:=db+1;
    szin[db]:=extractword(1,s,[' ']);
    vidam[db]:=strtoint(extractword(2,s,[' ']));
  end;
  close(f);
  min:=1;
  for i:=2 to db do if vidam[i]<vidam[min] then min:=i;
  for i:=1 to db do if vidam[i]=vidam[min] then writeln(szin[i]);
  readln;
END.

 

49. Írj ki egy színt, amelynek a vidámsága 3! (Nem biztos, hogy van. Ahhoz, hogy ennek a működését teszteld, két különböző bemeneti fájllal kell kipróbálnod, az egyikben van 3, a másikban nincs.)

megoldás

USES Sysutils,Strutils;
VAR f:text;
    i,db:integer;
    s:string;
    szin:array[1..100] of string;
    vidam:array[1..100] of integer;
BEGIN
  assign(f,'feladat6.txt');
  reset(f);
  db:=0;
  while not eof(f) do begin
    readln(f,s);
    db:=db+1;
    szin[db]:=extractword(1,s,[' ']);
    vidam[db]:=strtoint(extractword(2,s,[' ']));
  end;
  close(f);
  i:=1;
  while (i<=db) and (vidam[i]<>3) do i:=i+1;
  if i<=db then writeln(szin[i])
  else writeln('nincs ilyen');
  readln;
END.  

 

50.  Írd ki a színeket ábécérendben! (Az ékezetes karakterekre nem lesz jó, de nem baj.)

megoldás

USES Sysutils,Strutils;
VAR f:text;
    i,j,db,min:integer;
    s:string;
    szin:array[1..100] of string;
    vidam:array[1..100] of integer;
    mv:integer;
    ms:string;
BEGIN
  assign(f,'feladat6.txt');
  reset(f);
  db:=0;
  while not eof(f) do begin
    readln(f,s);
    db:=db+1;
    szin[db]:=extractword(1,s,[' ']);
    vidam[db]:=strtoint(extractword(2,s,[' ']));
  end;
  close(f);
  for i:=1 to db-1 do begin
    min:=i;
    for j:=i+1 to db do if szin[j]<szin[min] then min:=j;
    mv:=vidam[i]; vidam[i]:=vidam[min]; vidam[min]:=mv;
    ms:=szin[i]; szin[i]:=szin[min]; szin[min]:=ms;
  end;
  for i:=1 to db do writeln(szin[i],' ',vidam[i]);
  readln;
END. 

 

13. Irány az érettségi!

Ebben a fejezetben bemutatom egy érettségi feladat teljes megoldását, és a megoldás célszerű menetét. Ezzel le is zárjuk az alapvető programozási eszközök témáját, és a következő fejezettel belekezdünk a grafikus alkalmazások készítésébe.

A mintafeladat a 2012. év nyári érettségije lesz. Először nagyon alaposan olvassuk el a feladat leírását! Egy bemeneti mintafájlt feltöltöttem az oldalra.

A nagyvárosokon belül, ha csomagot gyorsan kell eljuttatni egyik helyről a másikra, akkor sokszor a legjobb választás egy kerékpáros futárszolgálat igénybevétele. A futárszolgálat a futárjainak a megtett utak alapján ad fizetést. Az egyik futár egy héten át feljegyezte fuvarjai legfontosabb adatait, és azokat eltárolta egy állományban. Az állományban az adatok rögzítése nem mindig követi az időrendi sorrendet. Azokra a napokra, amikor nem dolgozott, nincsenek adatok bejegyezve az állományba.
A fájlban legalább 10 sor van, és minden sor egy-egy út adatait tartalmazza egymástól szóközzel elválasztva. Az első adat a nap sorszáma, ami 1 és 7 közötti érték lehet. A második szám a napon belüli fuvarszám, ami 1 és 40 közötti érték lehet. Ez minden nap 1-től kezdődik, és az aznapi utolsó fuvarig egyesével növekszik. A harmadik szám az adott fuvar során megtett utat jelenti kilométerben, egészre kerekítve. Ez az érték nem lehet 30-nál nagyobb.
Például:
1 1 5
1 2 9
3 2 12
1 4 3
3 1 7

A 3. sor például azt mutatja, hogy a hét harmadik napján a második fuvar 12 kilométeres távolságot jelentett.

Az adatok beolvasását (1. feladat) már meg is tervezhetjük. Minden út három adatot tartalmaz, melyeket három tömbben (nap, fuvar, hossz) tárolunk. Mivel naponta legfeljebb 40 út lehet, ezért a tömbök maximális mérete 280. Darabolással, konvertálással most nem lesz gond.

db:=0;

while not eof(f) do begin

  db:=db+1;

  readln(f,nap[db],fuvar[db],hossz[db]);

end;


Most olvassuk el az összes feladatot, és gondoljuk végig, van-e olyan rész, amely több feladatban is szerepel!

2. Írja ki a képernyőre, hogy mekkora volt a hét legelső útja kilométerben! Figyeljen arra, hogy olyan állomány esetén is helyes értéket adjon, amiben például a hét első napján a futár nem dolgozott!
3. Írja ki a képernyőre, hogy mekkora volt a hét utolsó útja kilométerben!
4. Tudjuk, hogy a futár minden héten tart legalább egy szabadnapot. Írja ki a képernyőre, hogy a hét hányadik napjain nem dolgozott a futár!
5. Írja ki a képernyőre, hogy a hét melyik napján volt a legtöbb fuvar! Amennyiben több nap is azonos, maximális számú fuvar volt, elegendő ezek egyikét kiírnia.
6. Számítsa ki és írja a képernyőre, hogy az egyes napokon hány kilométert kellett tekerni!
7. A futár az egyes utakra az út hosszától függően kap fizetést az alábbi táblázatnak megfelelően:

1 – 2 km 500 Ft
3 – 5 km 700 Ft
6 – 10 km 900 Ft
11 – 20 km 1 400 Ft
21 – 30 km 2 000 Ft

Kérjen be a felhasználótól egy tetszőleges távolságot, és határozza meg, hogy mekkora díjazás jár érte! Ezt írja a képernyőre!
8. Határozza meg az összes rögzített út ellenértékét! Ezeket az értékeket írja ki a dijazas.txt állományba nap szerint, azon belül pedig az út sorszáma szerinti növekvő sorrendben az alábbi formátumban:

1. nap 1. út: 700 Ft
1. nap 2. út: 900 Ft
1. nap 3. út: 2000 Ft

9. Határozza meg, és írja ki a képernyőre, hogy a futár mekkora összeget kap a heti munkájáért!

 

Látható, hogy a 2., 3., és 8. feladat mindegyike megoldható az utak időrend szerinti növekvő rendezésével, míg az eredeti sorrendre nincs szükség. Ezért beolvasás után rendezhetjük az utakat (többszempontú rendezés).

for i:=1 to db-1 do begin

  min:=i;

  for j:=i+1 to db do

    if (nap[j]<nap[min]) or ((nap[j]=nap[min]) and (fuvar[j]<fuvar[min]))
    then min:=j;

  x:=nap[min]; nap[min]:=nap[i]; nap[i]:=x;
  x:=fuvar[min]; fuvar[min]:=fuvar[i]; fuvar[i]:=x;
  x:=hossz[min]; hossz[min]:=hossz[i]; hossz[i]:=x;

end;

 

A 4. és 5. feladat megoldásához megszámolhatjuk, melyik nap hány fuvar volt. Erre egy napifuvar[1..7] tömböt használunk, melynek 7 eleme a hét egyes napjaira vonatkozó fuvarok száma. Mivel a ciklusban nap[i] egy 1 és 7 közötti szám, a napifuvar[nap[i]] kijelöli a napifuvar tömb megfelelő elemét.

for i:=1 to 7 do napifuvar[i]:=0;

for i:=1 to db do napifuvar[nap[i]]:=napifuvar[nap[i]]+1;

 

A 6. feladat ehhez hasonló, csak nem a fuvarok számát, hanem összhosszát kell kiszámítani (napihossz[1..7] tömb).

for i:=1 to 7 do napihossz[i]:=0;
for i:=1 to db do napihossz[nap[i]]:=napihossz[nap[i]]+hossz[i];

 

A 7., 8. és 9. feladat mindegyike felhasználja az útdíjtáblázatot. Ezért célszerű egy függvényt írni, amely adott hosszra megadja az útdíjat, és a feladatokban ezt a függvényt hívni.

function utdij(hossz:integer):integer;

 begin

  if hossz<3 then ututdij:=500

  else if hossz<6 then utdij:=700

  else if hossz<11 then utdij:=900

  else if hossz<21 then utdij:=1400

  else utdij:=2000;

 end;

 

Ennyi előkészület után nekiláthatunk a feladatoknak.

2. Ez hossz[1] lesz.

3. Ez pedig hossz[db].

4. Kiírjuk 1 és 7 között azokat az i-ket, melyekre napifuvar[i]=0.

5. Maximumkiválasztás napifuvar tömbre.

6. Az adatok a napihossz tömbben vannak.

7. Az utdij függvény hívása.

8. Végigmegyünk a három tömbön, és mindegyik lépésben kiírjuk utdij(hossz[i])-t.

9. Összegezzük az utdij(hossz[i])-ket.

Egyszerű volt, ugye?

Feladat

tavok.txt

51. Írd meg a programot!

megoldás

USES Sysutils,Strutils;
VAR f:text;
    i,j,db,min,max,x:integer;
    nap,fuvar,hossz:array[1..280] of integer;
    napifuvar,napihossz:array[1..7] of integer;

Function utdij(hossz:integer):integer;
 begin
  if hossz<3 then utdij:=500
  else if hossz<6 then utdij:=700
  else if hossz<11 then utdij:=900
  else if hossz<21 then utdij:=1400
  else utdij:=2000;
 end;

BEGIN
  // beolvasás
  assignfile(f,'tavok.txt');
  reset(f);
  db:=0;
  while not eof(f) do begin
    db:=db+1;
    readln(f,nap[db],fuvar[db],hossz[db]);
  end;
  closefile(f);
  // rendezés
  for i:=1 to db-1 do begin
    min:=i;
    for j:=i+1 to db do
      if (nap[j]<nap[min]) or ((nap[j]=nap[min]) and (fuvar[j]<fuvar[min]))
      then min:=j;
    x:=nap[min]; nap[min]:=nap[i]; nap[i]:=x;
    x:=fuvar[min]; fuvar[min]:=fuvar[i]; fuvar[i]:=x;
    x:=hossz[min]; hossz[min]:=hossz[i]; hossz[i]:=x;
  end;
  // fuvarszámlálás
  for i:=1 to 7 do napifuvar[i]:=0;
  for i:=1 to db do napifuvar[nap[i]]:=napifuvar[nap[i]]+1;
  for i:=1 to 7 do napihossz[i]:=0;
  for i:=1 to db do napihossz[nap[i]]:=napihossz[nap[i]]+hossz[i];
  //megoldás
  writeln('2. feladat: ',hossz[1],' km');
  writeln('3. feladat: ',hossz[db],' km');
  write('4. feladat:');
  for i:=1 to 7 do if napifuvar[i]=0 then write(i:2);
  writeln;
  max:=1;
  for i:=2 to 7 do if napifuvar[i]>napifuvar[max] then max:=i;
  writeln('5. feladat: ',i);
  write('6. feladat: adjon meg egy távot! ');
  readln(x);
  writeln(' Ehhez tartozó útdíj: ',utdij(x),' Ft');
  writeln('7. feladat:');
  for i:=1 to 7 do writeln(' ',i,'. nap: ',napihossz[i],' km');
  assignfile(f,'dijazas.txt');
  rewrite(f);
  for i:=1 to db do
    writeln(f,nap[i],'. nap ',fuvar[i],'. út: ',utdij(hossz[i]),' Ft');
  closefile(f);
  x:=0;
  for i:=1 to db do x:=x+utdij(hossz[i]);
  writeln('9. feladat: ',x,' Ft');
  write('enter: ');
  readln;
END.

 

 

14. Az első grafikus alkalmazás

A Lazarus IDE segítsége már szöveges alkalmazásoknál is jól érzékelhető volt. Egy egyszerűbb grafikus alkalmazásnál pedig a kód jelentős részét a Lazarus készíti el. A fejlett (4. generációs) szerkesztőben a grafikus felületet összerakhatjuk egérrel, az egyes objektumokat kezelő kód egy része automatikusan megjelenik. A Lazarus alkotóinak határozott elképzelése volt az alkalmazás elkészítésének folyamatáról. Ha ezt követjük, az IDE segítségünkre lesz. Természetesen lehetséges másképpen, más sorrendben is elkészíteni a programot, akkor viszont a Lazarus inkább akadályoz. Ismerkedjünk meg egy egyszerű programon keresztül a fejlesztés lépéseivel!

Eseményvezérelt programozás

Egy grafikus program működése alapjában különbözik az eddig írt programok stílusától. Az eddigi programjaink imperatív stílusban íródtak, vagyis előírták, a programnak milyen műveleteket, milyen sorrendben kell elvégezni. (A strukturált, procedurális programozás ennek a stílusnak a továbbfejlesztése, mely a program logikus szerkezetére és a részfeladatok eljárásokra bontására koncentrál.)

A grafikus program azonban idejének nagy részében arra vár, hogy bekövetkezzen valamilyen esemény (gombra kattintás, ablak mozgatása...), ekkor lefut a beépített vagy általunk megírt eseménykezelő, majd a program tovább vár. Itt tehát a program által végrehajtott tevékenységek sorrendje a külső hatásoktól függ. Ez az eseményvezérelt programozás.

A programunk fő része az eseménykezelő ciklus:

Ismételd
  ha van értesítés eseményről, hajtsd végre az eseménykezelőjét
  ha van változás a felületen, frissítsd
amíg nincs vége

A grafikus felületet a Windows kezeli, ez számunkra nagy könnyebbség, hiszen a megjelenítést (gombok, szövegmezők), a grafikus elemek kezelését (gomb lenyomása, szöveg beírása) nem nekünk kell elkészíteni. Ha a Windows egy eseményt észlel, üzenetet küld a programunknak (pl. "megnyomták az OK gombot"), ekkor az eseménykezelő ciklus lefuttatja az előre beállított eseménykezelőt. Ha az eseménykezelő valamit módosított a grafikus felületen (pl. egy gomb lenyomására megjelenített egy képet), annak kirajzolására az eseménykezelő futtatása után kerül sor.

Objektumorientált programozás

Az objektumok használata jelentősen megkönnyíti a grafikus felület programozását. Egy objektum nem csak adatokat tartalmazhat (mint pl. egy tömb), hanem az adatokat kezelő eljárásokat is. Így az adataival és az azokat kezelő eljárásokkal együtt egy zárt egységet alkot. A program működése során ezek az objektumok hatnak kölcsön. Pl. van egy Button1 nevű gombunk. Ennek van felirata, ez a Button1.Caption tulajdonság (a tulajdonság egy objektumban tárolt adat), és van egy Button1.OnClick eljárása (más néven metódusa), ami a gomb lenyomásakor végrehajtódó eseménykezelő eljárás. Ez az eseménykezelő tartalmazhat egy Label1.Caption:='start' utasítást, amely egy felirat (szintén objektum) szövegét módosítja. Látható, hogy az objektumot és tulajdonságát pont választja el.

Kezdetben előre elkészített objektumokat használunk, majd létrehozzuk saját objektumainkat.

Az első projekt

A Projekt->Új projekt menüből most az Alkalmazást válaszd. Mivel ez a projektünk mindenképpen több fájlból áll, rögtön mentsd is el egy külön mappába. Most több fájl mentésére is rákérdez a Lazarus. Azt elsőnél (ez az .lpi fájl) adj nevet a projektnek a korábbi szabályok szerint. A második fájl neve maradhat unit1.pas.

A főablakon (itt van a menüsor és jó pár eszköztár), a forráskód-szerkesztőn és az üzeneteken kívül két újabb elemre is szükségünk lesz.  Az egyik az objektumfelügyelő, a másik a formszerkesztő. Ha valamelyik eltűnik, használd a Nézet menüt.

A programunk legalább egy ablakot létre fog hozni a képernyőn, ezt az IDE rögtön el is készíti. Ezt hívják Form1-nek (többnyire megtartjuk az objektumok Lazarus által adott nevét). Az objektumfelügyelőben láthatjuk az egyes objektumok tulajdonságait és eseménykezelőit. Keresd meg a felügyelőben a Form1.Height tulajdonságot, majd a formszerkesztőben állítsd át egérrel az ablak magasságát! A formszerkesztővel az ablakunk és a benne lévő elemek helyzetét, méretét gyorsan beállíthatjuk.

Más tulajdonságokat a felügyelőben tudunk állítani. Állíts be háttérszínt (Form1.Color). A clRed és hasonló konstansok előre beállított számokat tartalmaznak, de használhatsz egyéni színt is. Módosítsd az ablak feliratát is (Form1.Caption). Ezután futtasd le a programot!

 

Ez a program még nem sokat tudott. Zárd be az ablakot. Az eszköztárról (Standard fül) válaszd ki az OK-val jelölt gombot (a buboréksúgó jelzi a típusát: TButton), és kattints a formszerkesztőben. Létrejött egy Button1 nevű gomb. Egészen pontosan Form1.Button1, mert a Form1 tartalmazza a Button1 objektumot. A Button1.Caption tulajdonságot írd át "Bezár"-ra.

Ettől a gomb még nem fogja bezárni a formot. Kattints duplán a gombra. Ekkor az IDE létrehozza a gomb megnyomásra reagáló eseménykezelőjének vázát. Adott neki egy logikus nevet. Ezt egészítsd ki így:

procedure TForm1.Button1Click(Sender: TObject);
begin
  Form1.Close;
end;

 

Próbáld ki a programot. Ezután figyed meg a következőket:

A Form1.Close begépelése közben az IDE megjelenít egy segédablakot, amelyből kiválaszthatod a Form1 megfelelő eljárását vagy tulajdonságát (ha mégsem, nyomj CTRL+szóközt). Mivel egy objektumnak rengeteg tulajdonsága van, nem tudjuk mindet megjegyezni, szerencsére az elnevezésük logikus.

A Close a Form1 objektum egy eljárása, mely bezárja az ablakot, egyúttal a fő eseménykezelő ciklusnak is vége lesz.

Elegendő lett volna csak a Close használata, mert most a Form1 eljárását adjuk meg, és az alapból a Form1 objektumból indul ki.

A felügyelőben a Button1 eseményeinél, az OnClick eseménynél megtaláljuk a legutóbbi eljárásunk nevét. Ezen kívül rengeteg más eseményt is látunk itt, az OnMouseEnter pl. akkor következik be, ha rávisszük az egeret a gombra.  Ehhez most nem rendelünk eseménykezelőt. Ennek ellenére ha a program futásakor ráviszed az egeret a gombra, valami változást látsz. Ennek az az oka, hogy a gombnak vannak alapértelmezett eseménykezelői, amelyek akkor is lefutnak, ha te nem adsz meg ilyet (ezek nagy részét a Windows kezeli).

Mi a különbség a Form1 és a TForm1 között?

Ezzel a magyarázattal most egy kicsit előreszaladunk; nem kell elolvasnod, később még visszatérünk rá.

A TForm egy Lazarusba beépített objektumtípus, más néven osztály. Ennek leszármazottja a TForm1 osztály, melyet kibővítettünk egyéb objektumokkal (pl. gomb) és eljárásokkal (pl. a gomb eseménykezelője). 

A Form1 egy olyan objektum, melynek típusa TForm1. A programunk indulásakor létrehozza a Form1 objektumot. Ebből következik, hogy a TForm1 osztály alapján akár több egyforma ablakot is készíthetnénk. (Ekkor viszont a gomb eseménykezelőjében már nem szerepelhetne Form1.Close, csak Close!)

A projekt bővítése

Szükség lesz a formon még egy gombra (Button2), és egy TLabel típusú feliratra (Label1).  Írd át a Labe1.Caption-t "..."-ra, a Button1.Caption-t pedig erre: "Szia program". Állítsd át a Label1.Font.Size-t 20-ra. Készíts eseménykezelőt Button2-höz:

procedure TForm1.Button2Click(Sender: TObject);
begin
  Form1.Label1.Caption:='Szia programozó!';
end; 

  (Most is elég lett volna csak Label1.Caption megadása.)

http://public.dkrmg.hu/_/rsrc/1506701484462/tananyagok/lutter/lazarus/f14/kep2.png

Feladatok

52. Készíts programot, amely tartalmaz egy "0" feliratú gombot, amely minden megnyomásra eggyel továbbszámol!

megoldás

procedure TForm1.Button1Click(Sender: TObject);
begin
  Button1.Caption:=inttostr(strtoint(Button1.Caption)+1);
end;

 

53. Készíts programot, amely tartalmaz egy olyan gombot, amely minden megnyomásra a form véletlenszerű helyére ugrik! Használd a Top (elem bal felső pontjának  távolsága a tartalmazó elem tetejétől), Left (elem bal felső pontjának  távolsága a tartalmazó elem bal szélétől), Height (magasság) és Width (szélesség) tulajdonságokat!

megoldás

procedure TForm1.Button1Click(Sender: TObject);
begin
  Button1.Left:=random(Form1.Width-Button1.Width);
  Button1.Top:=random(Form1.Height-Button1.Height);
end;   

 

 

15. A projekt felépítése

A projekt fájljai

Vessünk egy pillantást a projektünket tartalmazó mappára! A konzolalkalmazásoknál több fájlt láthatunk. Ha a projektünket pl. elso néven mentettük, akkor az elso.lpi a projekt információs fájl, mely XML formátumban tartalmazza, milyen fájlok tartoznak a projekthez, és azokat milyen szabályok szerint kell lefordítani.

A főprogram továbbra is az elso.lpr fájl. Ha ennek megnézzük a tartalmát, láthatjuk, hogy nem ezen a fájlon dolgoztunk a forrásszerkesztőben. Alkalmazás típusú projektben az .lpr fájl szerepe csak annyi, hogy létrehozza az általunk megszerkesztett formot (vagy formokat) , és elindítsa a fő eseménykezelő ciklust.
  Application.Initialize;
  Application.CreateForm(TForm1, Form1);
  Application.Run;

Az egyes formokhoz tartotó kód .pas fájlokban van, az első projektünknél ez a unit1.pas lesz (mivel a projektünk csak egy formból áll). Figyeljük meg, hogy a .lpr fájl elején szerepel egy uses..., Unit1 deklaráció!


A
unit1.pas fájl ismerős a forrásszerkesztőből. Figyeljük meg, hogy az elején nem program ..., hanem unit ... deklaráció szerepel, lévén ez egy unit, programmodul.

Hol vannak azok a kiindulási adatok, melyeket a formszerkesztőben és az objektumfelügyelőben adtunk meg, és a program indulásakor az objektumaink tulajdonságainak kezdőértékét adják? Ezek az értékek a unit1.lfm fájlba kerültek. Ha megézzük a unit1.pas-t, láthatunk benne egy {$R *.lfm} úgynevezett fordítási direktívát. Ez arra utasítja a fordítót, hogy a unit fordítsa közben a megadott fájlt is vegye figyelembe.

A mappában találunk még egy elso.ico-t, ez a projektünk ikonja. Az alapértelmezett ikon helyett készíthetünk másikat. Az elso.res bináris formában erőforrásokat tartalmaz, ilyenek lehetnek (az ikonon kívül) hangok, képek, melyeket hozzáfordíthatunk a programunk kódjához. Ezt az .lpr fájlban "húzza be" egy {$R *.res} fordítási direktíva. A másik lehetőség, hogy a programunk futás közben külső fájlból olvassa be ezeket az adatokat.

Fordításkor további fájlok keletkeznek egy lib alkönyvtárban, ezek a különböző komponensek lefordított változatai.

A unit felépítése

A programunk kódját többnyire a unit1.pas fájlba írjuk, ismerkedjünk meg ennek szerkezetével! A Lazarus sok mindent automatikusan létrehoz a kódban, de ha később ezen módosítani akarunk, tudnunk kell, mi hová való.

unit Unit1;

Ez jelzi a fordítónak, hogy unitról, vagyis programmodulról van szó. A unit neve megegyezik a forrásfájl nevével, tehát ne írd át, bízd a fordítóra a kezelését!

interface

Az interface szekció azokat az adatokat tartalmazza, melyeket a unitot használó Pascal program (amely tehát betölti a unitot a Uses segítségével) elérhet. Ezek a kívülről látható eljárások fejlécei, illetve olyan változók, konstansok, objektumok stb., melyeket a unitot betöltő program rendelkezésére kívánunk bocsátani.

Az interface részben az eljárások fejléce szerepel, majd az implementation részben megismételjük a fejlécet, de már az eljárás törzsét is megírjuk. Az ilyen típusú "előre" deklarálást forward deklarációnak nevezzük.

Az interface részben látható egy uses, ez a unit által betöltött további unitok listája. Ezt a listát az IDE készítette, ezek a unitok szükségesek pl. a form elkészítéséhez is. Ha azonban nem csak a grafikus eszközökkel rakjuk össze a programunkat, a listát esetleg bővítenünk kell.

Ezután következik egy type, ahol a TForm1 objektumtípus (osztály, vagyis class) megadása történik. A private és public szekciókkal nem foglalkozunk, mindent az alapértelmezett helyre vagy a public szekcióba írunk.

Végül a var deklarálja a Form1 objektumot.

implementation

Itt következik az interface részben megadott forward deklarációk kifejtése, vagyis az eljárások törzsének megadása. Továbbá itt helyezkedik el minden olyan változó és eljárás, amely a unitunk "magánügye", az őt betöltő program elől rejtett.

 

 

16. Egy összetett grafikus alkalmazás

Ebben a fejezetben összeállítunk egy egyszerű játékot, amelyben egy képernyőn feltűnő labdát kell elkapni minél többször. Még mindig az IDE eszközeivel dolgozunk, és nem készítünk saját objektumot.

A projekt alapjai

Hozz létre egy új projektet, és mentsd el egy saját mappába (jatek), jatek néven (mármint az .lpi fájlt, mert a unit1.pas neve marad).

A labdát tetszőleges rajzolóprogrammal elkészítheted, a kép mérete legyen 64x64 pixeles. Mentsd el labda.png néven (és png formátumban!) a jatek mappába.

Helyezz el a formon egy TImage típusú objektumot (az Additional fülön találod az eszköztáron), ez lesz az Image1. Keresd meg a Picture tulajdonságát, és ott töltsd be a képet. Az Image1.Height és Width tulajdonságot állítsd 64-re.

Mivel a kép mindig téglalap alakú tartomány, a formon a labda körül látszik a téglalap alakú háttér. Az átlátszóság kezelésére az egyszerűbb rajzprogramok nem alkalmasak, szerencsére ilyenkor is kiválaszthatunk a képen egy átlátszó színt. Ez a kép bal alsó pixelének a színe lesz. Állítsd az Image1.Transparent tulajdonságot True-ra!

Szükségünk lesz még egy indítógombra (TButton típusú Button1), és egy pontokat számoló feliratra (TLabel típusú Label1). A Label1.Font.Size tulajdonságot állítsd 20-ra, a Button1.Caption-t írd át! Eddig így néz ki:

labdás screenshot

Inicializálás

Vannak olyan műveletek, melyeket a program indításakor egy alkalommal végre kell hajtani (inicializálás, vagyis a kezdőértékek beállítása). Ez konzolalkalmazásnál egyszerűen a BEGIN utáni rész lenne. Egy eseményvezérelt programnál azonban minden eljárást egy esemény indít be.

Szerencsére van egy olyan esemény, amely a program indításakor egyszer megtörténik, ez pedig a form létrehozása.  Az objektumfelügyelőben lépj vissza a Form1-re, és keresd meg az OnCreate eseményt. A ...-ra kattintva az IDE létrehozza a TForm1.FormCreate eljárást. Itt most ez szerepeljen:

Label1.Caption:='Kapd el a labdát!';

Nem szerencsés, ha a labda máris látszik, folytassuk:

Image1.Visible:=False;

Időzítő

A labda bukkanjon fel 1 másodpercenként. Ez egy olyan esemény, amelyet nem felhasználói beavatkozás vált ki, hanem egy időzítő. Helyezz el a formon egy TTimer típusú objektumot (Timer1), ez a System fülön van. A program futása közben ez nem fog látszani. Legfontosabb tulajdonságai: Enabled (működik-e), Interval (hány ezredmásodpercenként váltsin ki eseményt) és maga az OnTimer esemény.

Az Interval értéke 1000 lesz (1 másodperc), viszont az Enabled legyen False, hogy a program indításakor még ne mozgassa a labdát.

Következzen az OnTimer esemény elkészítése. Ide beírhatnánk a labda odébbrakását, de gondoljuk végig, hogyan fog működni a játék. Ha a felhasználó rákattint a labdára, azt akkor is odébb kell tenni, hogy ismételt kattintásokkal ne lehessen egyszerre sok pontot szerezni. Ezért a labda odébbrakását célszerűbb külön eljárásba tenni. Ez az eljárás az implementation szekcióba kerüljön, de a TForm1 dolgai elé. Az 53. feladat alapján:

procedure hopp;
begin
  Form1.Image1.Left:=random(Form1.Width-Form1.Image1.Width);
  Form1.Image1.Top:=random(Form1.Height-Form1.Image1.Height);
end;

 

Mivel ez nem a TForm1 eljárása, mindenhová ki kell írnunk a Form1-et. A width utasítás használatával kijelölhető a következő utasításban használt objektum, így rövidebben:

  with Form1 do begin
    Image1.Left:=random(Width-Image1.Width);
    Image1.Top:=random(Height-Image1.Height);
  end; 

 

Az Image1.Height helyett írhattunk volna 64-et is, de így jobb, mert más méretű képpel is működik.

Most már csak a Timer1 OnTimer eseményét kell elkészítenünk, amely a hopp eljárást hívja meg.

Első kísérlet

Meg kell írnunk a Start gomb eseménykezelőjét. Ez bekapcsolja az időzítőt, és láthatóvá teszi a labdát. (Az eljárás vázát az IDE készítse el!)

procedure TForm1.Button1Click(Sender: TObject);
begin
  Timer1.Enabled:=True;
  Image1.Visible:=True;
end; 

 

A játék természetesen még nem működik, de érdemes ezt a változatát is kipróbálni, mert így egyszerűbb a hibakeresés.

Pontozás

Szükségünk lesz egy pont egész típusú változóra, amely a pontszámot tárolja:  ezt az implementation szekcióba tedd, a TForm1 dolgai elé!

A Button1Click-et egészítsd ki a pont nullázásával!

A programnak reagálnia kell a labda elkapására. Az Image1-nek is van OnClick eseménye. Ez növeli és ki is írja a pontszámot, majd odébb teszi a labdát. Ekkor az időzítőt is újra kell indítanunk, hogy megint 1 másodperctől számoljon vissza.

procedure TForm1.Image1Click(Sender: TObject);
begin
  pont:=pont+1;
  Form1.Label1.Caption:=IntToStr(pont);
  hopp;
  Form1.Timer1.Enabled:=False;
  Form1.Timer1.Enabled:=True;
end; 

 

Az IntToStr függvényre azért van szükség, mert a Caption típusa string.

És már működik is a játék!

 

 

 

 

17. Objektumok

Ebben a fejezetben megismerkedünk az objektumok és osztályok fogalmával. Ugyan írhatsz programot úgy, hogy csak a Lazarus beépített objektumait használod, a továbblépéshez mindenképpen szükséges saját objektumok létrehozása.

Kitérő: TYPE és CONST

Általunk létrehozott típusokat a deklarációs részben elláthatunk névvel. Így

VAR a:array[1..100] of integer;
    b:array [1..100] of integer;


helyett

TYPE nagytomb=array[1..100] of integer;
VAR a,b:nagytomb;


is használható. Ha a fontosabb típusoknak nevet adunk, a programkódunk olvashatóbb lesz, összetettebb típusok (osztályok) használatakor pedig a
type nélkülözhetetlen.

A konstansokat olyan változókként használhatjuk, melyeknek értékét nem lehet módosítani, és már fordítási időben értéket kapnak. Gondoljunk egy programra, amely szabadesést számol: a programban sok helyen felbukkan a 9.81 érték. Ha a deklarációs részbe beírjuk:

CONST g=9.81;
...
x:=g/2*sqr(t);

a programban mindenhol g szerepelhet a 9.81 helyett. Így a kód érthetőbb lesz, mert nem puszta számokat látunk, hanem neveket, amelyek a szám funkcióját is mutatják. Könnyebb dolgunk lesz akkor is, ha a programot a Föld helyett a Holdra kell alkalmazni: egyetlen helyen, a konstansdeklarációnál kell átírni. A fordító egyébként a kód elkészítésekor g-t mindenhol 9.81-gyel fogja helyettesíteni.

Az előző példában a tömb mérete 100, de mi van, ha ez változik? Át kell írnunk a 100-akat, nem csak a deklarációban, hanem a program tömbön végigmenő ciklusainál. Ráadásul nem használhatjuk a szerkesztő Csere funkcióját, mert máshol is szerepelhet 100 a programban, aminek a tömbmérethez nincs köze. Így viszont:

CONST meret=100;
VAR t:array[1..meret] of integer;
...
for i:=1 to meret do writeln(t[i]);

elég csak egy helyen módosítani.

A Pascal unitjai nem csak eljárásokat, hanem típusok és konstansdeklarációkat is tartalmaznak. Ilyen  pl. a színbeállításoknál használt clRed konstans.

Az eredeti objektumtípus, amit már ritkán használunk

Bár az objektumok központi szerepet játszanak napjaink programozástechnikájában, eredeti formájukban ritkán használják őket. A következő példaprogram konzolalkalmazás!
Létrehozunk egy
szakasz objektumtípust, és ennek alapján több szakasz típusú objektumot. Egy szakasznak van kezdőpontja (x1,y1) koordinátákkal, és végpontja (x2,y2). Ezek az objektumban tárolt változók, más néven tulajdonságok vagy mezők. Az objektum tartalmaz továbbá egy hossz függvényt, amely kiszámítja a szakasz hosszát (Pitagorasz tétele alapján), és egy beallit eljárást, amely beállítja a koordinátákat. Az objektum függvényeinek és eljárásainak neve metódus.

TYPE Szakasz=object
  x1,y1,x2,y2:real;
  procedure beallit(p,q,r,s:real);
  function hossz:real;
End;

procedure Szakasz.beallit(p,q,r,s:real);
begin
  x1:=p; y1:=q;
  x2:=r; y2:=s;
end;

function Szakasz.hossz:real;
begin
  hossz:=sqrt(sqr(x1-x2)+sqr(y1-y2));
end;

VAR a,b:Szakasz;

BEGIN
  a.beallit(10,10,20,30);
  b:=a;
  b.x2:=50;
  writeln(a.hossz);
  writeln(b.hossz);
  readln;
END. 

 

Először létrehoztuk az objektum típusát (Szakasz), melynek segítségével több Szakasz típusú objektumot deklarálhatunk (a és b). Az objektumok is változók. A típusdeklarációban megadtuk a tulajdonságok nevét és típusát, valamint a metódusok fejlécét. A metódusokat a típusdeklaráció után fejtettük ki.

A beallit metódus nélkül is tudnánk értéket adni a koordinátáknak (ahogy a b objektumnál történik), de így kényelmesebb.

A mezők a és b objektumban is létrejönnek, a metódusokból azonban csak egy példány létezik, és mindkét szakasz azokat használja - ilyen értelemben az objektum csak a mezőit tartalmazza, a metódosok a típushoz tartoznak.

Leszármazás és öröklődés

Hozzuk létre a teglalap objektumtípust is! Ha az oldalak párhuzamosak a koordinátatengelyekkel, a téglalap ugyanazokkal a koordinátákkal adható meg, mint a szakasz (szemközti csúcsok). Ezért a meglévő kódból felhasználjuk, amit lehet.

A teglalap típus a szakasz típusból származik, ugyanakkor ki is bővül egy új függvénnyel (terulet).

TYPE

Szakasz=object

  x1,y1,x2,y2:real;
  procedure beallit(p,q,r,s:real);
  function hossz:real;
End;

Teglalap=object(szakasz)
  function terulet:real;
End;

 

A téglalap örökli a szülő objektum minden tulajdonságát és metódusát, ugyanakkor újakkal bővíthető. (Sőt, az örökölt tulajdonságok és metódusok módosíthatóak a gyerek objektumban. Ehhez újra meg kell őket adni.) Nézzük az új metódust:

function Teglalap.terulet:real;
begin
  terulet:=abs(x1-x2)*abs(y1-y2);
end;

Osztályok és mutatók

Változókat (így objektumokat is) eddig statikusan hoztunk létre, ami azt jelenti, hogy ezek a változók már fordítási időben előkerülnek, és a program indulásakor fix memóriaterület lesz lefoglalva számukra. Ez nem praktikus akkor, ha előre nem látható mennyiségű adattal lesz dolga a programnak (ilyen esetben eddig a lehető legnagyobb méretű tömböt kellett deklarálni). Lehetőség van változók dinamikus létrehozására futási időben, az ezek által használt memóriaterület mérete a program futása közben rugalmasan változik.

A dinamikus memóriakezelés kulcsa a pointer, azaz mutató, amely egy memóriacímet tartalmazó változó. Az általa mutatott memóriaterületen tetszőleges típusú adat elhelyezkedhet. A pointert deklaráljuk, de az általa mutatott adatnak már futási időben foglalunk helyet.

Az osztály (class) objektumra mutató pointer típus. Nézzük az első programot objektum helyett osztállyal:

TYPE Szakasz=class 
  x1,y1,x2,y2:real;
  procedure beallit(p,q,r,s:real);
  function hossz:real;
END;

procedure Szakasz.beallit(p,q,r,s:real);
begin
  x1:=p; y1:=q;
  x2:=r; y2:=s;
end;

function Szakasz.hossz:real;
begin
  hossz:=sqrt(sqr(x1-x2)+sqr(y1-y2));
end;

VAR a,b:Szakasz;

BEGIN
  a:=Szakasz.Create;
  a.beallit(10,10,20,30);
  b:=a;
  b.x2:=50;
  writeln(a.hossz);
  writeln(b.hossz);
  readln;
END.

 

Mi a különbség? Először is, a Szakasz most nem objektumtípus, hanem objektumra mutató pointer típus, vagyis osztály (object helyett class).  Ezért a és b sem objektum, hanem szakasz objektumra mutató pointer. Ebből következik, hogy a program indulásakor nem léteznek objektumok, csak két pointerünk van.

A szakasz objektumunkat létre kell hozni, a memóriában le kell foglalni helyet a koordináták tárolására. Az objektumot a Szakasz osztály konstruktora hozza létre (minden osztálynak alapból van konstruktora), ennek neve Create. Figyeld meg, hogy a Create nem objektum metódusaként kerül meghívásra (akkor a.Create lenne), hanem az osztályhoz tartozik. Ez így logikus, hiszen a konstruktor meghívásakor maga az objektum még nem létezik.

A Create egyrészt létrehozza az objektum egy példányát (instance), másrészt, mint függvény, eredményként visszaadja az objektum memóriacímét. Ezt a címet tároljuk el a-ban. (Ha nem tárolnánk el, nem tudnánk hivatkozni az objektumra.)

A továbbiakban a fordító gondoskodik arról, hogy ha class típusú változóval találkozik, akkor a műveleteket a megfelelő objektummal (az osztály egy példányával) végezze el. A b.x2:=50 utasítás ugyanazt csinálja, akár objektum, akár osztály a b. Ugyanakkor nem szabad elfelejtenünk, hogy míg a b.x2 egy objektum adott tulajdonságára vonatkozik (ebben nincs különbség az objektumos és osztályos programváltozat között), az osztály típusú változók mégis csak mutatók!

A második példaprogramban egyetlen objektum jön létre, mert egyszer hívtuk meg a konstruktort. A b:=a értékadás a b mutatónak értékül adja az a-ban tárolt memóriacímet. Így két mutatónk van, és mind a kettő ugyanarra az objektumpéldányra mutat. Ezért a.hossz és b.hossz ugyanazt az eredményt adja.

Az objektumnak destruktora is van, mely felszabadítja a lefoglalt memóriaterületet. Ennek neve Destroy. Pl. a.Destroy. Ezt ritkábban használjuk a konstruktornál, mert egy objektumra gyakran hivatkoznak más objektumok, pl. egy form törlése előtt a benne lévő gomb objektumokat is törölni kell. Jó gyakorlat a Destroy helyett a FreeAndNil metódus, amely egyúttal a mutató értkét nil-re állítja (ez egy speciális mutató-érték, amely jelzi, hogy a mutatónk nem mutat érvényes objektumra).

Hol szerepel mindez a programban?

A fentiek fényében nézzünk át ismét egy grafikus alkalmazást. Legyen csak egy formunk, amelyen egy gomb van, és beállítottuk a gomb OnClick eseménykezelőjét.

TYPE

  { TForm1 }

  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  end;

 

Ezt a kódot a Lazarus állította elő. Figyeljük meg, hogy minden osztály neve T-vel kezdődik, az azonosítóknak hosszú nevük van, és felváltva tartalmaznak kis- és nagybetűket (camel case). Ezek a szabályok a programkód olvashatóságát és érthetőségét javítják.

A Lazarus minden osztálya a TObject osztály leszármazottja. Itt létrejön egy új TForm1 osztály, mely kibővíti a szülő TForm osztályt egy mezővel (Button1) és egy metódussal (Button1Click). A Button1Click-nek van egy paramétere: a Sender paraméter az eseményt kiváltó objektum - mivel ez most csak a Button1 lehet, ebben az esetben nincs jelentősége. Fontos lesz majd, ha ugyanazt az eseménykezelőt több objektum is meghívhatja. Figyeljük meg, hogy a Sender típusa nem TButton, hanem általános TObject, ez később fontos lesz.

Az eseménykezelők mind a TForm1 metódusai.

 

VAR
  Form1: TForm1;


Itt létrejön a
Form1 class típusú változó, a TForm1 típus alapján, amely tehát az objektumra mutató pointer, mellyel majd az objektumunkra (a program főablakára) hivatkozunk. Azonban joggal hiányolhatjuk a Form1:=TForm1.Create utasítást, az objektumot létrehozó konstruktort, amely sehol nem szerepel a kódban. Akkor hogyan keletkezik a form? A megoldás a .lpr fájlban szerepel, az Application.CreateForm(TForm1,Form1) utasítás hívja meg a konstruktort, de még sok mást is csinál, létrehozza a formon lévő többi objektumot is (hiszen a Button1 konstruktorát is meg kell hívni).

 

A következő fejezetben mindezeket egy konkrét példán mutatom be.

 

 

18. Objektumok létrehozása

A feladat

Készítsünk egy programot, amely egy beadott egész szám minden számjegyéhez létrehoz egy gombot! Mivel nem tudjuk előre, hány számjegyünk lesz, nem gyárthatjuk le tervezési időben a gombokat. A gombok futási időben jönnek létre, ezért a tervezőt nem használhatjuk a gombok tulajdonságainak beállítására, ezt is futási időben kell megtenni. Legyen egy gomb mérete 32×32 pixel, a gombok közötti távolság 10 pixel.

Előkészítés

Kezdetben legyen a formon egy szövegdoboz (Edit1, ide írjuk be a számot), és egy indítógomb (Button1).

http://public.dkrmg.hu/_/rsrc/1506701484669/tananyagok/lutter/lazarus/f18/kep4.png


A gombra dupla kattintással hozd létre az eseménykezelő üres vázát, a kód többi részét majd ebbe fogjuk írni.

A gombok létrehozása

Az eseménykezelőben szükségünk lesz egy i változóra, amely végigmegy Edit1 karakterein. Ezenkívül deklarálnunk kell egy TButton típusú objektumot (valójában osztályt!) is, amely az éppen létrehozott gombra hivatkozik. Ezután minden karakterhez (melyek számjegyek) létrehozunk egy gombot:

procedure TForm1.Button1Click(Sender: TObject);
var i:integer;
    g:TButton;
begin
 for i:=1 to length(Edit1.Caption) do begin
   g:=TButton.Create(Form1);
   g.Parent:=Form1;
   g.Height:=32;
   g.Width:=32;
   g.Top:=50;
   g.Left:=i*10+(i-1)*32;
   g.Caption:=Edit1.Caption[i];
   g.Font.Size:=18;
 end;
end;

 

Először létrehozzuk az adott gombot, majd beállítjuk a tulajdonságait.

Mint látható, a
TButton konstruktorának van egy paramétere. Ezt onnan tudhatjuk, hogy a Create begépelésekor megvárjuk (vagy CTRL+szóköz lenyomásával előhívjuk) a szerkesztő súgóját, mely kiegészíti a begépelt azonosítót, és itt a listában láthatjuk a Create paraméterét. Mivel ennyi azonosítót nem tudunk megjegyezni, ez a súgó nagyon hasznos segédeszköz, főként, hogy az azonosítók nevéből azok feladata is sokszor kitalálható.

A konstruktor paramétere azt adja meg, hogy melyik másik objektum a tulajdonosa a gombnak. Ez azért fontos, mert a form bezárásakor a Lazarusnak nem csak a formot, hanem az általa birtokolt objektumokat is meg kell semmisíteni, és ezt a tulajdonosi adatok alapján végzi el.

A másik új tulajdonság a Parent: ez grafikus objektumoknál azt jelzi, hogy az adott objektum melyik másik objektum belsejében jelenik meg (továbbá a Top, Left értékek is a Parent bal felső sarkához képest értendők).

A Left értékét a gomb sorszámából, a gombok szélessége és a térközök alapján állapítottuk meg.

Eseménykezelők és típuskényszerítés

Bonyolítsuk a feladatot! Egy újonnan létrehozott gomb megnyomásakor a rajta lévő számjegy értéke nőjön 1-gyel (a 9-ből meg legyen 0). Ehhez a gombok eseménykezelőjét is be kell állítani, most már a szerkesztő segítsége nélkül.

Figyeld meg a Button1 eseménykezelőjét! Ennek mintájára el kell készítenünk egy saját eseménykezelőt, de az összes új gomb eseménykezelője lehet ugyanaz az eljárás (hiszen mindegyik gomb esetén ugyanazt csinálja).

Honnan fogja tudni az eseménykezelő, melyik gomb megnyomása váltotta ki az eseményt? Ezt a célt szolgálja a Sender paraméter, mely tartalmazza az eseményt kiváltó objektum címét.
Legegyszerűbb, ha az új eseménykezelő fejlécét a meglévő másolásával hozod létre.

Először a form osztályát egészítjük ki az új metódus fejlécével:

  TForm1 = class(TForm)
    Button1: TButton;
    Edit1: TEdit;
    procedure Button1Click(Sender: TObject);
    procedure SajatClick(Sender:TObject);

 

Majd megírjuk az eljárást:

procedure TForm1.SajatClick(Sender:TObject);
var g:TButton;
begin
 g:=TButton(Sender);
 g.Caption:=IntToStr((StrToInt(g.Caption)+1) mod 10);
end;

 

Az egyszerűség kedvéért deklaráltunk egy g változót, mely az adott gombra mutat.

Logikusan g:=Sender lenne az utasítás, de ez fordításkor hibaüzenetet ad. Ennek az az oka, hogy a Sender típusa általános TObject, míg g típusa TButton. Ezért a típuskényszerítés (typecast) műveletével meg kell adnunk, hogy a Sender-ben tárolt értéket tekintse úgy, mint egy TButton típusú mutató. A típuskényszerítés általános formája:

típus(kifejezés)

Ekkor a Pascal a kifejezés eredeti típusától függetlenül azt a megadott típusúnak tekinti. Ez többnyire csak akkor működhet, hogyha a kifejezés által lefoglalt memóriaméret megegyezik az új típus által igényelttel. Például a

writeln(byte('a'));

utasítás 97-et ír ki, ugyanis az 'a' karakter tároláskor a memóriában egy bájtot foglal, melynek értéke 97 (az 'a' ASCII-kódja).

A számjegy léptetése oda-vissza alakít szám és szöveg között, a 9-ről 0-ra ugrást pedig feltétel helyett a tízes maradék képzésével oldja meg.

Ha elindítod a programot, semmi nem fog történni a gombok nyomogatásakor. Ennek az az oka, hogy a létrehozott gombok OnClick tulajdonságára még nem állítottuk be az eseménykezelő eljárásunkat. Egészítsd ki a gomb tulajdonságainak beállítását a következő sorral:

   g.Font.Size:=18;
   g.OnClick:=@SajatClick;

 

Egy objektum OnClick tulajdonsága valójában egy mutató, mely az eseménykezelő eljárás memóriacímét kell, hogy tartalmazza. A @ művelet megadja egy azonosító (esetünkben az eseménykezelő eljárás) memóriacímét.

A forráskód az oldal alján letölthető.

Értékelés

Nincs tökéletes program, ez erre a példaprogramra különösen igaz. Érdemes elemezni a fejlesztési lehetőségeket.

Hibalehetőség, hogy "gombosításkor" nem vizsgáltuk a beírt szöveg karaktereit. Ha ezek nem számjegyek, az StrToInt függvény hibaüzenetet ad futáskor.

Ha még egyszer megnyomjuk a gombot, a korábban létrehozott számjegyes gombok megmaradnak, és továbbiak készülnek. Új gombok létrehozásakor a régieket meg kellene szüntetni. Azonban a már létrehozott gombokhoz nem tudunk hozzáférni, mert a g változó mindig csak az éppen létrehozott objektumot mutatja.  Ezeket a mutatókat el kellett volna tárolnunk.

 

 

19. Dinamikus adatszerkezetek

A programunkban használt változókat a program elején deklarálnunk kell. Ezeknek a memóriát a program futása kezdetén lefoglalja. A tömbök méretét is fordítási időben kell megadnunk, azon később már nem változtathatunk: ezért mindig a lehető legnagyobb tömböt deklaráljuk, legfeljebb nem használjuk ki. Ezek az adatok egy stack nevű memóriaterületen helyezkednek el, statikus adatoknak nevezzük őket.

Gondoljuk el a következő példát: emberek mozgását követjük két helyszín között, melyeket két tömb ír le. Ha valaki egyik helyszínről átmegy a másikra, az egyik tömbből kivesszük, a másikba betesszük. Így mindkét tömb mérete az emberek maximális száma kell, hogy legyen, vagyis pontosan kétszer annyi memóriát foglalunk le, mint amennyit egy időben felhasználunk.

Másik példa: egy program, mely a képernyőre kattintva egy új objektumot hoz létre. Ezek száma tetszőleges, mekkora tömböt deklaráljunk a tárolásukra?

Valójában már eddig is használtuk a dinamikus memóriaterületet. Deklarálunk egy osztályt: var g:TButton, majd létrehozunk egy objektumot: g:=TButton.Create(Form1).  A g mutató a statikus memóriaterületen van, az általa mutatott objektum viszont futási időben jön létre. A memória foglalása csak a Create végrehajtásának pillanatában történik meg. A dinamikusan, futás közben létrehozott objektumokat a program a heap nevű memóriaterületen tárolja. Ezt korlátozhatjuk, de lehet akár a teljes rendelkezésre álló szabad memória is.

Lista és rekurzív típusdeklaráció

Figyeljük meg a következő típust!

TYPE lancszem=class

  adat:integer;

  mutato:lancszem;

end;

 

Egy lancszem típusú objektum tartalmaz egy egész számot és egy osztály típusú mutatót. A mutató egy újabb, lancszem típusú objektumra mutat. A fordító képes értelmezni ezt a rekurziót (önhivatkozást). Ez a mutató fogja megmutatni a memóriában a következő láncszemet. A következő példában látható, hogyan lehet tetszőleges számú egész számot ebben a láncszemekből álló listában tárolni. Ha egy mutató nem mutat sehová  (a lánc utolsó eleme), értékét NIL-re állítjuk. Ez egy speciális mutató-érték, jelzi, hogy a mutató nem mutat sehová.

VAR fej:lancszem; //mutató a lista első elemére, fejére
    utolso:lancszem; //mutató a lista utolsó elemére
    n:integer;
BEGIN
  fej:=nil;
  Repeat
    readln(n);
    if n>0 then begin
      if fej=nil then begin
        fej:=lancszem.Create;
        utolso:=fej;
      end else begin
        utolso.mutato:=lancszem.Create;
        utolso:=utolso.mutato;
      end;
      utolso.adat:=n;
      utolso.mutato:=nil;
    end;
  Until n=0;
END.

 

A fejmutató csak egyszer kap értéket, amikor az első elemet helyezzük el a listában. Új elem berakásakor létrehozunk a heap-en egy új láncszem objektumot, és az eddigi utolsó elem mutatóját ráállítjuk.

 

A lista bejárása mindig a fejétől kezdődik, és elemenként történik, mert minden elemre csak az őrá mutató elem által juthatunk.

 

utolso:=fej;
  while utolso<>nil do begin
      writeln(utolso.adat);
      utolso:=utolso.mutato;
  end;

 

Ha a listából elemet törlünk a destruktor segítségével, az őt megelőző elem mutatóját arra az elemre (vagy nil-re) kell állítani, amelyre eredetileg a törölt elem mutatott. Elem beszúrásakor is ügyelni kell a mutatók módosítására.

Lista vagy tömb?

A lista bizonyos szempontból jobb, más szempontból rosszabb a tömbnél.

A lista mellett szól:

  • mérete dinamikusan változtatható
  • adott elem törlése, elem beszúrása gyors, mert a listaelemeket nem kell mozgatni a memóriában, elég csak két mutató értékét módosítani

A tömb mellett szól:

  • a tömb adott sorszámú elemét gyorsan megkaphatjuk (indexelés), míg a lista 100. eleméhez a fejtől kiindulva végig kell lépegetni az előző  elemeken
  • a memóriafelhasználás rugalmatlan, de tervezhető

Ha tehát a tömbünkön jellemzően for-ciklussal lépegetünk végig, használhatunk helyette listát. Ha azonban sokszor ugrálunk a tömbelemek között, a tömb használata gyorsabb programot eredményez.

Ha a tömbös programunk elindul, a tömb biztosan befért a memóriába. A lista viszont, mivel mérete folyamatosan nőhet, egyszer csak kinőheti a memóriát.

Ha dinamikusan növekvő adatszerkezetre van szükségünk, csak a lista jöhet szóba. Az indexelés okozta sebességcsökkenésre megfelelő algoritmusokkal kell megoldást találnunk.

Beépített lista típus

Szerencsére a Free Pascal rendelkezik beépített listatípussal, melyhez a lista kezelését megkönnyítő metódusok is tartoznak.  Sajnos a TList class által megvalósított lista csak mutatókat képes tárolni, ellentétben az előző példával, ahol a listaelem adatmezője integer.

A TList legfontosabb eljárásai és függvényei:

  • Create
  • Count: megadja a lista elemszámát
  • Add(mutató): a mutatót, mint adatot, a lista végéhez fűzi (tárolja)
  • Remove(mutató): megkeresi a mutató első előfordulását, és törli a listából
  • Delete(sorszám): az adott sorszámú elemet törli a listából; a számozás 0-val kezdődik!
  • Items[sorszám]: tömbként viselkedik, megadja az adott sorszámú mutatót

 

Látható, hogy így a listát tömbhöz hasonlóan tudjuk kezelni.  A fenti példát nézzük meg ezzel a listatípussal is! Mivel a listaelemekben nem tudunk számokat tárolni, külön létre kell hoznunk számot tároló objektumokat, ez lesz a TSzam class.

USES Classes;

TYPE TSzam=class

  adat:integer;
end;

VAR sz:TSzam;

    lista:TList;
    n,i:integer;

BEGIN

  lista:=TList.Create;
  Repeat
    readln(n);
    if n>0 then begin
      sz:=TSzam.Create;
      sz.adat:=n;
      lista.Add(sz);
    end;
  Until n=0;

  for i:=0 to lista.Count-1 do
    writeln(TSzam(lista.Items[i]).adat);
  readln;
END.

 

Megjegyzések: a TList a Classes unitban van. Figyeljük meg a kiírásnál szereplő típuskényszerítést: a TList elemei egyszerű mutatók (Pointer típus), meg kell tehát adnunk, hogy TSzam-ként akarjuk kezelni.

 

 

20. Összetett példa dinamikus memóriakezeléssel

Feladat

Készítsünk programot, amely a formra kattintva pacákat hoz létre, a pacákra kattintva pedig eltünteti azokat!  A pacák mozogjanak is a formon!

Terv

A pacákat a form timer eseménye fogja mozgatni, ezért szükséges lesz egy ciklussal végigmenni a pacákon, tehát tárolnunk kell a pacák mutatóit. Erre a célra listát használunk.

A formon való kattintást nem az OnClick, hanem az OnMouseDown eseménnyel oldjuk meg, mert utóbbi az egérkoordinátákat is megkapja.

A formszerkesztővel egyedül a Timer1 objektumot hozzuk létre, a pacák mind futási időben készülnek.

A forráskód

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, ExtCtrls;

type

  { TForm1 }

  TForm1 = class(TForm)
    Timer1: TTimer;
    procedure FormCreate(Sender: TObject);
    procedure FormMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure Pacakatt(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
  private
    var lista:TList;
  public
    { public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
begin
  lista:=TList.Create;
end;

procedure TForm1.Pacakatt(Sender: TObject);
begin
  lista.Remove(Sender);
  Application.ReleaseComponent(TShape(Sender));
end;

procedure TForm1.Timer1Timer(Sender: TObject);
var i:integer;
    paca:TShape;
    dx,dy:integer;
begin
 for i:=0 to lista.Count-1 do begin
   paca:=TShape(lista.Items[i]);
   dx:=random(3)-1;
   dy:=random(3)-1;
   paca.Left:=paca.Left+dx;
   paca.Top:=paca.Top+dy;
   if paca.Left<0 then paca.Left:=0;
   if paca.Left>Width-paca.Width then paca.Left:=Width-paca.Width;
   if paca.Top<0 then paca.Top:=0;
   if paca.Top>Height-paca.Height then paca.Top:=Height-paca.Height;
 end;
end;

procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var paca:TShape;
begin
 paca:=TShape.Create(Form1);
 paca.Shape:=stCircle;
 paca.height:=32;
 paca.Width:=32;
 paca.Brush.Color:=128+random(128)+256*(128+random(128))+65536*(128+random(128));
 paca.Brush.Style:=bsSolid;
 paca.Parent:=Form1;
 paca.Top:=y-paca.height div 2;
 paca.Left:=x-paca.width div 2;
 paca.OnClick:=@Pacakatt;
 lista.Add(paca);
end;

end.

A forráskód magyarázata: előkészületek

Ha a formszerkesztőt használjuk TShape létrehozására, a Lazarus a szükséges unitot beilleszti a kódba (Uses ExtCtrls). Most ezt nekünk kell megtenni. Jó módszer, ha a formszerkesztővel létrehozunk egy TShape-et, majd rögtön töröljük: ebből kiderül, mi a szükséges unit.

Hasonlóképpen, a pacára kattintás eseménykezelőjének fejlécét is kipuskázhatjuk a Lazarusból, ha a szerkesztővel hozunk létre eseménykezelőt. Az egyes pacák eseménykezelője is legyen a Lazarus filozófiája szerint a TForm1 osztály eljárása; ezt most nekünk kell beírni a TForm1 osztály típusdeklarációjába.

procedure Pacakatt(Sender: TObject);

 

A szerkesztővel hozzuk létre a form OnCreate és OnMouseDown eseménykezelőjét!

A globális lista

Egyetlen globális változónk a lista, amely a paca objektumok mutatóit tárolja. Mivel az összes paca a formon belül van, ezt az objektumot is a TForm1 osztályban deklaráljuk, ráadásul a private szekcióban, mert csak a form eseménykezelői fogják használni.

A listát létre kell hoznunk a program indulásakor:

procedure TForm1.FormCreate(Sender: TObject);
begin
  lista:=TList.Create;
end;

Pacák létrehozása

Paca a formra kattintáskor keletkezik. A Form1MouseDown megkapja az egérmutató X és Y koordinátáit, ráadásul a form bal felső sarkához képest. Az eseménykezelő segédváltozója a paca, amely az éppen létrehozott TShape objektumra mutat.

procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var paca:TShape;

 

A most létrehozott objektum minden lényeges tulajdonságát itt kell beállítani. Először létrehozzuk az objektumot:

paca:=TShape.Create(Form1);

 

majd beállítjuk a tulajdonságait. A Parent felelős azért, hogy a paca a Form1-en belül jelenjen meg. A színek beállítása véletlenszerű. A helyzetnél ügyelünk arra, hogy a Left és Top a bal felső sarok koordinátáit adja meg, mi viszont az egérrel a kör közepét jelöljük ki:

paca.Shape:=stCircle;
 paca.height:=32;
 paca.Width:=32;
 paca.Brush.Color:=128+random(128)+256*(128+random(128))+65536*(128+random(128));
 paca.Brush.Style:=bsSolid;
 paca.Parent:=Form1;
 paca.Top:=y-paca.height div 2;
 paca.Left:=x-paca.width div 2;

 

végül beállítjuk az eseménykezelőt, és a létrehozott objektum mutatóját eltároljuk a listában:

 paca.OnClick:=@Pacakatt;
 lista.Add(paca);

Objektum törlése

A Pacakatt eljárásban Sender paraméter a kattintott paca mutatója. Mi történne, ha a pacát TShape(Sender).Destroy metódusával törölnénk?

A programunk futási hibával leállna (érvénytelen memóriaterületre hivatkozás). Ha ugyanis a pacára kattintunk, lefut az OnClick eseményen kívül az alapértelmezett OnMouseDown és OnMouseUp esemény is, utóbbi az OnClick után. Ha viszont az OnClick már törölte az objektumot, az OnMouseUp érvénytelen hivatkozást tartalmaz (a Sender paramétere olyan memóriaterületre mutat, ami már nincs lefoglalva).

Megtehetjük, hogy az utolsónak lefutó OnMouseUp eseményt használjuk. Ez azért lenne rossz gyakorlat, mert nem fogjuk tudni minden egyes objektumnál követni, melyik a legutolsó eseménykezelője.

Ezért használjuk az Application.ReleaseComponent eljárást, amely az objektumot törlésre jelöli, de csak az eseménykezelők lefutása után törli. Ha egy objektumot a saját eseménykezelőjében törlünk, ez a javasolt módszer.

Törlés előtt még az objektum mutatóját ki kell venni a listából:

procedure TForm1.Pacakatt(Sender: TObject);
begin
  lista.Remove(Sender);
  Application.ReleaseComponent(TShape(Sender));
end;

 

Figyeljük meg a típuskényszerítés használatát: a lista csak egyszerű mutatókat tartalmaz, a fordító nem tudja, milyen típusú objektumra mutat!

Mozgatás

A Timer1 egy ciklussal végigmegy a listán (ne feledjük, hogy 0-tól indexelt!), és az egyes pacákat elmozdítja a 8 irány egyikébe (vagy helyben hagyja). Gondoskodnunk kell arról is, hogy a pacák ne lépjenek ki a formból.

procedure TForm1.Timer1Timer(Sender: TObject);
var i:integer;
    paca:TShape;
    dx,dy:integer;
begin
 for i:=0 to lista.Count-1 do begin
   paca:=TShape(lista.Items[i]);
   dx:=random(3)-1;
   dy:=random(3)-1;
   paca.Left:=paca.Left+dx;
   paca.Top:=paca.Top+dy;
   if paca.Left<0 then paca.Left:=0;
   if paca.Left>Width-paca.Width then paca.Left:=Width-paca.Width;
   if paca.Top<0 then paca.Top:=0;
   if paca.Top>Height-paca.Height then paca.Top:=Height-paca.Height;
 end;
end;