19. fejezet: 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
A Free Pascal rendelkezik beépített listatípussal, melyhez a lista kezelését megkönnyítő metódusok is tartoznak. 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. Viszont egy mutató tetszőleges adatot tároló objektumra mutathat.
A TList legfontosabb eljárásai és függvényei:
CreateCount: megadja a lista elemszámátAdd(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ólDelete(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 őket kezelni.