20. Dynamické premenné


Posledná zmena: 28.11.2002

Banner Text 28.11.2002

    čo sme sa doteraz naučili

    • poznáme globálne a lokálne premenné
    • niektoré premenné sú referenciami iných premenných, napr. inštancie objektov

    čo sa budeme dnes učiť

    • ako sa pracuje s dynamickými premennými, ako sa vytvárajú a ako sa rušia
    • ukážeme použite netypového smerníka a netypových formálnych parametrov

Dynamické premenné

  • mnohé premenné, ktoré sme doteraz používali boli statické:
  • ich veľkosť a adresa v pamäti bola určená už počas kompilácie
  • globálne premenné (premenné hlavného programu, resp. všetkých unitov) sa vyhradia v jednom dátovom segmente
  • lokálne premenné (aj parametre) podprogramov sa automaticky vyhradia pri volaní podprogramu (automaticky sa zrušia pri ukončení podprogramov) ale ich veľkosť a pozícia v pamäti (t.j. v systémovom zásobníku) sa určí počas kompilácie
  • statické premenné nemôžu zmeniť svoju veľkosť a adresu počas behu (run time)

Delphi má mechanizmus na vytváranie dynamických premenných:

  • až počas behu (t.j. pomocou príkazov programu, t.j. run time) sa vyhradí pre ne pamäť
  • tieto premenné môžeme vytvárať alebo rušiť v ľubovoľnom momente počas behu programu
  • premenné sa nevytvárajú ani v dátovom segmente programu ani v zásobníku ale v špeciálnom segmente, ktorý je určený pre všetky dynamické premenné heap (halda)
  • už sme pracovali so špeciálnymi prípadmi dynamických premenných: dynamické polia (array of), reťazce (string) a objekty (class)

Dynamické premenné sú prístupné len pomocou tzv. smerníka (referencia, adresa, odkaz), t.j. musíme mať smerníkovú premennú (premenná, ktorá obsahuje referenciu na inú premennú), pomocou ktorej budeme mať prístup k danej dynamickej premennej

Smerníkové premenné

  • sú premenné, ktoré sa používajú na prístup k dynamickým (ale niekedy aj ku statickým) premenným
  • smerníky sú často statické premenné
  • pri štarte programu majú nedefinovanú hodnotu
  • smerníková premenná zaberá 4 bajty (adresy v počítači sú 4-bajtové)
  • každá smerníková premenná môže adresovať (ukazovať, referencovať) len na premennú konkrétneho typu (dynamická premenná môže byť ľubovoľného typu, napr. číslo, reťazec, pole, množina, záznam, ...), t.j. ak aj ešte nie je nedefinovaná, vždy presne vieme typ dynamickej premennej, na ktorú odkazuje
  • pri definovaní typu smerník, môže byť znak ^ len pred identifikátorom! typu:
       type Preal=^real; Pint=^integer; PPoint=^TPoint;
    ale nie:
       type Pzaznam=^record x,y:integer end; Parr=^array[1..5] of byte; ...
  • do smerníkovej premennej môžeme priradiť
    • buď smerník už existujúcej premennej:
        var i:integer;
            s:^integer;
        begin
          s:=@i;   
      // adresa existujúcej premennej
          S^:=7;
    • alebo môžeme vytvoriť novú dynamickú premennú a jej adresu priradiť do smerníkovej premennej:
        var s:^integer;
        begin
          new(s);   
      // adresa novovytvorenej dynamickej premennej
          s^:=7;
  • s premennou, na ktorú odkazuje smerník, môžeme pracovať rovnako ako s "obyčajnou" premennou: ku smerníkovej premennej pripíšeme znak ^ (strieška)
  • smerníkové premenné môžeme navzájom priradzovať, resp. porovnávať na rovnosť alebo nerovnosť jedine ak sú identického typu

Štandardné procedúry new a dispose

    new(var p:^typ);

    - parametrom ľubovoľná smerníková premenná

  1. "zabudne" pôvodnú hodnotu p
    - ak bolo p nedefinované, tak to zrejme nevadí
    - ak p odkazovala na dynamickú premennú (a nik iný), tak k nej strácame prístup
  2. vyhradí vo voľnej časti pamäti (heap) veľkosť sizeof(typ)
    - väčšinou je to trochu viac (závisí od organizácie správy pamäti, ktorá sa stará o heap)
  3. do p priradí adresu tejto novej dynamickej premennej
  4. dispose(p:^typ);

  1. zaradí do voľnej pamäte (heap) premennú p^
  2. p má nedefinovanú hodnotu (nemala by sa ďalej používať)
    - všetky smerníky, ktoré odkazovali na túto istú dynamickú premennú, majú tiež nedefinovanú hodnotu
    - dynamickej premennej v heape sa pravdepodobne poškodila hodnota - už ju nesmieme používať

HEAP = časť pamäti pre všetky dynamické premenné (závisí od OS - často niekoľko 100 MB)

Konštantný smerník NIL

  • univerzálna konštanta pre všetky smerníkové typy
  • používa sa vtedy, keď potrebujeme rozlíšiť prípad, že smerníková premenná odkazuje na nejakú dynamickú premennú a že ešte neodkazuje na žiadnu
  • v Delhpi sa totiž nedá zistiť, či má nejaká (nielen smerníková) premenná nedefinovanú hodnotu
        p4:=nil;
  • p4 už nemá nedefinovanú hodnotu a pritom sa bude dať otestovať, že neodkazuje na žiadnu dynamickú, napr.:
        if p4=nil then new(p4);
  • so smerníkmi sú povolené len (relačné) operácie = a <> aj to len so smerníkmi rovnakého typu, resp. s nil
    • if (p1=p2) or (p1=nil) then ...; // je o.k.
      if p1<>p3 then ...;     // ak sú rôznych typov, je to chybný - neprípustný test

Niektoré zásady slušného programovania

  • snažíme sa, aby všetky smerníkové premenné mali buď nil alebo skutočne ukazovali na nejaké premenné (t.j. neboli nedefinované)
  • ak p=nil => odkaz p^ hlási známu chybu Access Violation a preto sa v programe často vyskytuje otázka
    if p=nil, prípadne if p<>nil …
  • nikdy neodkazujeme smerníkovou premennou, o ktorej nie sme 100% presvedčení, že je definovaná a rôzna od nil (radšej buďme pesimisti a všetko kontrolujme)

Príklady práce so smerníkmi

dynamická premenná celé číslo:

var s:^integer;
begin
  new(s); s^:=0;
  for i:=1 to 10 do s^:=s^+i;
  writeln(t,s^);
  dispose(s); s:=nil;
  ...

dynamický záznam:

type zaznam=record x,y:integer end;
var z:^zaznam;
begin
  new(z); z^.x:=100; z^.y:=200;
  with z^ do g.MoveTo(x,y);
  inc(z^.x,100); dec(z^.y,50);
  g.LineTo(z^.x,z^.y);
  ...

dynamické jednorozmerné pole:

type pole=array[1..10] of real;
var p:^pole;
begin
  new(p); for i:=1 to 10 do read(t,p^);
  for i:=9 downto 1 do p^[i]:=p^[i]+p^[i+1];
  ...

problém s veľkým poľom ako lokálna premenná:

type pole=array[1..1000000] of integer;
procedure test;
var p:pole;
begin
  p[1]:=1;
end;
  • takéto pole chce vzniknúť na zásobníku počas volania tejto procedúry - systém má ale problém s tak veľkým poľom

veľké pole ako lokálna premenná:

type pole=array[1..1000000] of integer;

var
  n:integer;

procedure test;
var
  p:^pole;
begin
  new(p);
  inc(n); p^[1]:=n;
  Form1.Memo1.Lines.Add(IntToStr(p^[1]));
  // dispose(p);

end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  while true do test;
end;
  • ak sa toto veľké pole pred koncom procedúry neuvoľňuje (dispose), program po niekoľkých prechodoch spadne na správe "Out of memory." - môžete takto otestovať veľkosť systémového zásobníka ...

smerník na objekt:

type tt=class a:integer; constructor Create(aa:integer); end;
constructor tt.Create(aa:integer); begin a:=aa; end;
var s:^tt;
begin
  new(s); s^:=tt.Create(8);
  writeln(t,s^.a);
  s^.Free; dispose(s);
end;

jednorozmerné pole smerníkov:

type
  mnozina=set of byte;     // 32 bajtov
  pole=array [1..1000] of ^mnozina;
                 // 4000 bajtov -- inak bez ^ by bolo 32000 bajtov
var
  data:pole;
  i:integer;
begin
  for i:=1 to 1000 do new(data[i]);
  for i:=1 to 1000 do data[i]^:=[random(256)];
...
  for i:=1 to 1000 do dispose(data[i]);
  // uvoľnený heap -- môže sa ďalej v tomto projekte používať
end;

príklady jedno- a dvoj-rozmerných polí smerníkov:

type
  pole=array[1..10] of integer;
  Ppole=^pole;
  pole2=array[1..20] of pole;      // obyčajné 2-rozmerné pole
  Ppole2=^pole2;
  pole2ppole=array[1..20] of Ppole;
var
  a:pole2ppole;
  b:Ppole2;
  i,j:integer;
begin
  for i:=1 to 20 do new(a[i]);
  for i:=1 to 20 do
    for j:=1 to 10 do
      a[i]^[j]:=i+j;
  new(b);
  for i:=1 to 20 do
    for j:=1 to 10 do
      b^[i][j]:=i+j;
...
end;

smerník na pole:

type
  spole=array[1..10] of ^integer;    // pole smerníkov na integer
  Pspole=^spole;
  sppole=array[1..20] of Pspole;
  Psppole=^sppole;
var
  c:sppole;
  d:Psppole;
  i,j:integer;
begin
  for i:=1 to 20 do new(c[i]);  // sizeof(c)=20*4
  for i:=1 to 20 do             // sizeof(c[i])=4
    for j:=1 to 10 do begin     // sizeof(c[i]^)=10*4
      new(c[i]^[j]);
      c[i]^[j]^:=i+j;
    end;
  new(d);
  for i:=1 to 20 do new(d^[i]); // sizeof(d)=4; sizeof(d^)=20*4
  for i:=1 to 20 do             // sizeof(d^[i])=4
    for j:=1 to 10 do begin     // sizeof(d^[i]^)=10*4
      new(d^[i]^[j]);
      d^[i]^[j]^:=i+j;
    end;
  ...
end;

záznamy, polia a smerníky:

type
  pole=array[1..10] of integer;
  Ppole=^pole;
  zazn=record
    x:pole;
    y:Ppole;
    z:array[1..10] of ^integer;
  end;
  Pzazn=^zazn;
  polePzazn=array[1..20] of Pzazn;
  PpolePzazn=^polePzazn;
var
  a:Pzazn;
  b:polePzazn;
  c:PpolePzazn;
  i,j:integer;
begin
  new(a);
  for i:=1 to 20 do new(b[i]);
  new(c); for i:=1 to 20 do new(c^[i]);
  for i:=1 to 20 do begin
    for j:=1 to 10 do c^[i]^.x[j]:=i+j;
    new(c^[i]^.y);
    for j:=1 to 10 do c^[i]^.y^[j]:=i+j;
    for j:=1 to 10 do new(c^[i]^.z[j]);
    for j:=1 to 10 do c^[i]^.z[j]^:=i+j;
  end;
...
end;
  •  

postupné prečítanie premennej c^[i]^.y^[j]

c
c^
c^[i]
c^[i]^
c^[i]^.y
c^[i]^.y^
c^[i]^.y^[j]

- je typu PpolePzazn = smerník
- je typu polePzazn = array
- je typu Pzazn = smerník
- je typu zazn = record
- je typu Ppole = smerník
- je typu pole = array
- je typu integer

  • zápis "smerníkovanej" premennej môžeme skrátiť: znak ^ vynecháme, ak si ho vedia Delphi jednoznačne domyslieť, t.j. ak za ^ nasleduje bodka "." alebo hranatá zátvorka "["

Smerník na smerník

nasledujúci príklad len ilustruje nezvyčajné použitie smerníkov:

type
  Pint=^integer;
  PPint=^Pint;
  PPPint=^PPint;
var
  i:Pint;
  j:PPint;
  K:PPPint;
begin
  new(i); i^:=123;
  new(j); new(j^); j^^:=345;
  new(k); new(k^); new(k^^); k^^^:=567;
...
end;

Správa pamäti (Memory management)

  • táto správa sa stará o udržiavanie obsadených a uvoľnených častí dynamickej pamäti (heap)
  • štandardné procedúry: New, Dispose, GetMem, ReallocMem a FreeMem - využívajú správu pamäti
  • každý vyhradený pamäťový blok (napr. pomocou New) má dĺžku zaokrúhlenú na najbližší násobok 4 a obsahuje ešte 4-bajtovú hlavičku - dĺžku bloku a iné stavové informácie
  • správa udržiava tieto dve premenné:
    • AllocMemCount - počet pamäťových blokov
    • AllocMemSize - dĺžka vyhradených pamäťových blokov
  • funkcia GetHeapStatus vráti ďalšie užitočné informácie o správe pamäti
  • rezervované slovo nil je špeciálna smerníková konštanta - vnútorne je reprezentovaná 4 bajtami s hodnotou 0
  • smerníkový operátor @premenná vráti smerník (referenciu - adresu) na danú premennú (neskôr uvidíme aj smerník na procedúru) - výsledkom je smerník typu ^typ, ak je typ typom premennej
  • smerníková aritmetika: pomocou štandardných procedúr inc a dec môžeme posúvať hodnotu smerníka o dĺžku typu, na ktorý odkazuje

malá ukážka smerníkovej aritmetiky:

var
  p:^integer;
  a:array[1..10] of integer;
  i:integer;
begin
  p:=@a[10];
  for i:=1 to 10 do begin
    p^:=i; dec(p);
  end;
  for i:=1 to 10 do
    Memo1.Lines.Add(IntToStr(a[i]));
end;

druhý príklad smerníkovej aritmetiky:

var
  p1,p2:^integer;
  a:array[1..10] of integer;
  i:integer;
begin
  for i:=1 to 10 do a[i]:=i;
  p1:=@a[1]; p2:=@a[10];
  for i:=1 to 5 do begin
    p1^:=p1^+p2^; p2^:=p1^-p2^; p1^:=p1^-p2^;
    inc(p1); dec(p2);
  end;
  for i:=1 to 10 do
    Memo1.Lines.Add(IntToStr(a[i]));
end;

všetky doterajšie smerníky boli presne zadaného typu (^typ) - priraďovanie a referencovanie smerníkových premenných bolo prísne kontrolované prostredím Delphi

Netypový smerník - typ POINTER

univerzálny smerník (podobne ako nil)

  • kompatibilný so všetkými smerníkmi: môžeme ho priradiť do smerníkovej premennej ľubovoľného typu a naopak (môže to byť veľmi nebezpečné - ľahko môžeme stratiť kontrolu nad smerníkmi)
  • nemôžeme pomocou neho pracovať s dynamickou premennou, na ktorú odkazuje (buď ho priradíme do typového smerníka, alebo ho pretypujeme)
  • keď s ním chceme pracovať, tak buď najprv do neho priradíme už nejaký "hotový" smerník na dynamickú premennú alebo vyhradíme novú dynamickú pamäť pomocou štandardnej procedúry GetMem(premenná_typu_pointer,dĺžka) - je to podobné New:
    • New(p) === GetMem(p,SizeOf(p^));
  • takto vyhradenú pamäť uvoľníme štandardnou procedúrou FreeMem(premenná_typu_pointer) - je to podobné Dispose:
    • Dispose(p) === FreeMem(p);
  • použitie uvidíme pri netypových formálnych parametroch:

Netypový formálny parameter

= formálny parameter, ktorý nemá uvedený typ

  • v tele procedúry ho môžeme použiť nasledujúcimi spôsobmi:
    • pretypovaním na konkrétny typ
    • alebo pomocou direktívy absolute (je to pretypovanie počas deklarácií)
    • alebo poslať ako netypový parameter do inej procedúry
      • napr. štandardná procedúra Move(odkiaľ, kam, koľko_bajtov)
      • alebo zápis, resp. načítanie do/z netypového súboru – budeme vidieť neskôr

napr. procedúra na výmenu obsahov dvoch ľubovoľných (rovnako veľkých) premenných:

procedure vymen(var a,b; dlzka:integer);
var
  t:Pointer;
begin
  GetMem(t,dlzka);
  Move(a,t^,dlzka); Move(b,a,dlzka); Move(t^,b,dlzka);
  FreeMem(t);
end;

použitie operátora @ a smerníkovej aritmetiky inc

function dump(var a; dlzka:integer):string;
var
  p:^byte;
begin
  Result:=''; p:=@a;
  while dlzka>0 do begin
    Result:=Result+IntToHex(p^,2)+' ';
    inc(p);
    dec(dlzka);
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  s:array[0..10] of char;
  i:integer;
begin
  s:='Ahoj Delphi';
  Label1.Caption:=dump(s,sizeof(s));
  i:=12345;
  Label2.Caption:=dump(i,sizeof(i));
end;

Direktíva ABSOLUTE

  • prekrytie premennej iným menom premennej (aj iného typu) - nejaká časť pamäti dostane ďalšie nové meno - je to veľmi nebezpečné

iná verzia šestnástkového výpisu kusu pamäti:

function dump2(var a; dlzka:integer):string;
var
  p:array[1..100] of byte absolute a;
  i:integer;
begin
  Result:='';
  if dlzka>100 then dlzka:=100;
  for i:=1 to dlzka do
    Result:=Result+IntToHex(p[i],2)+' ';
end;

Dynamické polia

  • reprezentované sú smerníkom na jednorozmerné pole
  • deklarácia nealokuje pamäť (mali by sme priradiť nil)
  • SetLength vyhradí pamäť (niečo ako GetMem) - ak už premenná mala vyhradené nejaké pole, tak toto sa automaticky uvoľní (niečo ako FreeMem)
  • ak X a Y sú premenné rovnakého typu dynamické pole, potom X :=Y spôsobí, že X referencuje na to isté pole ako Y (netreba alokovať pamäť pre X) - Delphi si teraz pamätá, že na toto pole sa odkazuje dvomi premennými a pamäť uvoľní, až keď sa zmenia referencie oboch polí;
  • nepoužívajte procedúry New, GetMem a pod. a ani operátor ^

v príklade:

var
  A,B:array of integer;
begin
  SetLength(A,4);
  A[0]:=1;
  B:=A;            // teraz sú obe polia v pamäti identické
  B[0]:=2;         // aj hodnotou A[0] je 2
  SetLength(B,3);  // teraz sú obe polia v pamäti na rôznych miestach
end;
  • pri porovnávaní premenných typu dynamické pole sa porovnávajú ich referencie a nie nie hodnoty polí

napr.

var
  A,B:array of integer;
begin
  SetLength(A,1);
  SetLength(B,1);
  A[0]:=2;
  B[0]:=2;
  if A = B then ...
end;
  • A=B vráti false ale A[0]=B[0] vráti true
  • na skrátenie dynamického poľa sa môže použiť aj Copy, napr. A:=Copy(A,5,10);
  • premenná typu dynamické pole zaberá 4 bajty = smerník na dynamicky alokované pole
    - buď je to nil alebo smerník na blok pamäti, ktorý je o 8 bajtov dlhší ako veľkosť poľa
    • 4 bajty použité na počet referencií
    • 4 bajty na počet prvkov poľa (Length)
    • za tým nasledujú prvky poľa
  • viacrozmerné dynamické pole je reprezentované úplne rovnako -- je to dynamické pole smerníkov na dynamické polia

Znakové reťazce - String

  • sú podobné dynamickým poliam - tiež sú to smerníky na polia znakov
  • podobne sa pamätá aj počet referencií a aktuálna dĺžka reťazca (Length)
  • za posledným znakom v poli je vždy #0 (nedá sa indexovať) - vďaka tomu je použiteľný aj ako #0 ukončený reťazec
  • prázdny reťazec je uchovaný ako nil (ale do stringovej premennej sa nemôže priradiť nil)
  • nemôžeme používať ani New ani Dispose ani iné procedúry správy pamäti, nemôžeme používať ^

teraz by sme už mohli pochopiť tento príklad:

procedure TForm1.FormCreate(Sender: TObject);
var
  s:string;
begin
  s:='ahoj Delphi';
  label1.Caption:=dump(s[1],Length(s));
        // nefunguje iba dump(s,Length(s)); - s je smerník
end;

Znakové reťazce ukončené #0 (null-terminated strings)

  • sú podobné znakovým reťazcom v C a C++
  • niekedy ich treba poznať pri práci so systémom Windows na nižšej úrovni
  • je to postupnosť znakov ukončená znakom #0
  • buď v znakom poli - dolná hranica je 0 (napr. TFileName = array[0..259] of Char;) alebo v dynamickej pamäti
  • preddefinovaný typ PChar (smerník na postupnosť znakov, t.j. ^char)

pracovať s takýmito reťazcami môžeme

  • v štandardných procedúrach a funkciách: Read, Readln, Str, Val, Write, Writeln, Val, AssignFile, Rename
  • štandardne ako s (dynamickým) poľom znakov
  • pomocou veľkej množiny špeciálnych funkcií pre takéto reťazce, napr.
    • StrCat    zreťazenie
    • StrComp   porovnanie
    • StrCopy   skopírovanie
    • StrLen    dĺžka reťazca
  • môžeme využiť aj smerníkovú aritmetiku

nasledujúci príklad ilustruje prácu s "null-terminated strings":

procedure TForm1.FormCreate(Sender: TObject);
var
  p,q:PChar;
begin
  GetMem(p,100);     // mohli by sme použiť aj StrNew alebo StrAlloc
  StrCopy(p,'milujem delphi');
  q:=p;
  while q^<>#0 do begin
    q^:=Upcase(q^);
    inc(q);          // posun na nasledujúci znak reťazca
  end;
  Label1.Caption:=p;
  Label2.Caption:=dump(p,StrLen(p)+1);
  FreeMem(p);
end;

Inštancie tried

  • každá premenná objekt - t.j. inštancia nejakej triedy je smerník na dynamicky alokovaný blok pamäti
  • treba na to myslieť pri porovnávaní aj priraďovaní (napr. g:=Image1.Canvas, alebo if g.Pen=Form1.Canvas.Pen then ...)
  • všetky stavové premenné sú uchované podobne ako v type záznam
  • informácie o metódach sa ukladajú do tabuľky VMT - virtual method table - je jediná pre všetky inštancie danej triedy
  • okrem metód obsahuje aj iné informácie o inštancii, napr. informácie o dĺžke, triede
  • je jasné, prečo je nezmysel namiesto k:=Kor.Create použiť k.Create -- k je nedefinované alebo nil a teda ním nemôžeme referencovať


© 2002 AB, KVI
blaho@fmph.uniba.sk