X.  

TYPY  UKAZATEL


Hodnotami typu ukazatel jsou paměťové adresy. Na operační paměť lze pohlížet jako na posloupnost bytů, jejichž pozici v této posloupnosti charakterizuje právě adresa. Pro účely adresování je posloupnost členěna na odstavce — úseky o délce 16 bytů.

Na strojové úrovni se k adresování používají dvojice šestnáctibitových registrů procesoru. Jeden z registrů dvojice obsahuje hlavní, tzv. segmentovou část adresy (segment), druhý doplňkovou, tzv. ofsetovou část adresy (ofset). Interpretujeme-li segment i ofset jako celá nezáporná čísla (na šestnácti bitech tedy čísla 0 až 65 535), má segment význam pořadí referovaného odstavce od začátku operační paměti a ofset pořadí referovaného bytu od začátku tohoto odstavce. Pořadí referovaného bytu od začátku operační paměti — tzv. efektivní adresa — je pak hodnotou výrazu segment × 16 + ofset.

Uvedené členění adresy na segment a ofset je zachováno i ve vnitřním tvaru ukazatelových hodnot v Turbo Pascalu. Hodnota ukazatele má velikost dvě slova (4 byty), přičemž nižší z nich obsahuje ofsetovou a vyšší segmentovou část reprezentované adresy.

X.1. 

Ukazatelové proměnné

Hodnotou ukazatelové proměnné je ukazatel na počátek nějakého úseku operační paměti. Aby referovaný úsek mohl být programem zpracován, musí být obvykle k dispozici ještě informace o jeho velikosti a struktuře, tedy informace o typu proměnné, za kterou bude pokládán. Význam termínů ukazatelová proměnná, ukazatel a referovaná proměnná ilustruje následující obrázek.

Obrázek 45: Ukazatelem referovaná proměnná

Třídy ukazatelových typů   Informaci o typu referované proměnné lze programu poskytnout dvěma způsoby, jimž odpovídají dvě implementované třídy ukazatelových typů — ukazatele typové a ukazatele univerzální (netypové).

Typ proměnné, referované typovým ukazatelem, je pevně stanoven již v popisu příslušného ukazatelového typu (obrázek 46), jako tzv. bázový typ ukazatele. Může jím být libovolný pojmenovaný typ, string nebo file. Každá proměnná, referovaná typovým ukazatelem, je pak považována za proměnnou jeho bázového typu.

Obrázek 46: Popis typu ukazatel

Jako univerzální jsou chápány ukazatele předdeklarovaného typu Pointer. Jejich univerzálnost spočívá v tom, že typy jimi referovaných proměnných jsou nedefinované — lze je podle aktuální potřeby volit až v okamžiku (každého) přístupu k těmto proměnným.

type Baze     = array [1..5] of Char;
     Ukazatel = ^Baze;

var  U1       : Ukazatel;
     U2       : Ukazatel;
     U3, U4   : ^Baze;
     U5       : ^Baze;
     P        : Pointer;
Paměťový úsek, referovaný kterýmkoliv z ukazatelů U1U5, bude pokládán za proměnnou typu Baze. Identita bázových typů ukazatelů však neznamená identitu ani kompatibilitu typů ukazatelových. V uvedeném příkladu jsou proměnnými identických typů pouze dvojice U1, U2U3, U4. Navzájem různých (a tudíž nekompatibilních) typů jsou například dvojice proměnných U1, U3 nebo U3, U5. Proměnná P je univerzálním ukazatelem, je tedy kompatibilní se všemi ukazatelovými typy a jí referovaný paměťový úsek je proměnnou neznámého typu.

Typovou kontrolu kontrolu kompatibility ukazatelových typů lze řídit direktivou {$T} kompilátoru. Ve stavu {$T–} je kontrola striktní — dva ukazatelové typy jsou kompatibilní pouze pokud jsou identické nebo pokud jeden z nich je univerzálním ukazatelovým typem. Ve stavu {$T+} jsou za kompatibilní považovány i ukazatelové typy s identickými bázovými typy.

Přiřazovací příkaz   Pro ukazatele je kompatibilita vůči přiřazení ekvivalentní kompatibilitě (viz výše). Pokud je tedy proměnná na levé straně přiřazovacího příkazu typovým ukazatelem, hodnotou výrazu na straně pravé musí být typový ukazatel identického typu nebo typový ukazatel s identickým bázovým typem (jen za stavu {$T+}) nebo ukazatel univerzální. Pokud je však proměnná univerzálním ukazatelem, smí být výraz ukazatelem libovolným.

Ukazatelový výraz může být tvořen ukazatelovou proměnnou či konstantou, voláním funkce s ukazatelovou hodnotou nebo aplikací adresního operátoru.

Ukazatelové funkce   Pro definici ukazatelových hodnot jsou jazykem implementovány funkce Ptr, Seg, OfsAddr.

function Addr (Objekt): Pointer;

Funkce vrací ukazatele na uvedený parametr, jímž může být libovolná proměnná nebo identifikátor uživatelského podprogramu. Mechanismus výpočtu funkční hodnoty je podle druhu parametru modifikován takto:

function Ptr (Segment, Ofset: Word): Pointer;

Vrací ukazatelovou hodnotu, odpovídající zadané segmentové (parametr Segment) a ofsetové (parametr Ofset) části reprezentované adresy.

function Seg (Objekt): Word;
function Ofs (Objekt): Word;

Skutečným parametrem obou funkcí může být libovolná proměnná nebo identifikátor uživatelského podprogramu. Funkce Seg resp. Ofs pak vrací segmentovou resp. ofsetovou část adresy uvedeného parametru, přičemž je mechanismus výpočtu funkční hodnoty modifikován podle druhu parametru stejně jako ve výše popsané funkci Addr.

Adresní operátor   Adresní operátor @ je operátorem unárním, váže se tedy na jediný operand vpravo. Používá se jako ekvivalent výše popsané adresní funkce Addr — výsledkem operace @Objekt je Addr (Objekt).

Typ výrazu @Objekt, kde Objekt je neprocedurální proměnná, závisí (na rozdíl od typu funkce Addr) na nastavení direktivy {$T} kompilátoru. Ve stavu {$T–} je typem výrazu ukazatel univerzální, kdežto ve stavu {$T+} ukazatel typový, jehož bázovým typem je typ proměnné Objekt.

Relační operace   Hodnoty kompatibilních ukazatelových typů lze porovnávat relačními operacemi býti rovno (operátor =) a býti různo (operátor <>). Při porovnávání není počítána efektivní adresa, porovnávají se zvlášť segmentové a zvlášť ofsetové části obou operandů (rovnost ukazatelů je dána rovností jejich segmentových částí i ofsetových částí). Tímto způsobem získaný výsledek relace však nemusí být vždy shodný s očekávaným. Například výraz

Ptr (100, 0) <> Ptr (99, 16)
má hodnotu True, i když 100 × 16 + 0 = 99 × 16 + 16. Hodnoty uvedených ukazatelů jsou tedy ve smyslu implementovaných relací různé, ačkoliv reprezentují stejnou efektivní adresu. Jedině ukazatele vracené procedurami NewGetMem pro zakládání dynamických proměnných (viz dále) jsou normovány (jejich segment je zvolen tak, že ofset je z intervalu 0 .. 15), takže jsou vždy porovnány korektně (rovnost platí právě tehdy, když oba ukazatele referují stejné místo v operační paměti).

Přístup k referované proměnné   Syntax přístupu k ukazatelem referované proměnné popisuje diagram na obrázku 47. Operandem, uvedeným před kvalifikátorem ^, musí být ukazatelová proměnná či konstanta, volání ukazatelové funkce nebo ozávorkovaný obecný výraz ukazatelového typu (například aplikace adresního operátoru @). Výsledkem operace je proměnná (!), alokovaná v operační paměti na adrese, získané vyhodnocením operandu.

Obrázek 47: Přístup k referované proměnné

Typ výsledné proměnné závisí na typu uvedeného operandu. Jestliže je operandem ukazatel typový, je typem výsledné proměnné jeho bázový typ. Pokud je však operandem ukazatel univerzální, je typ výsledné proměnné nedefinovaný. Proměnné nedefinovaného typu je pak nutno nějaký typ přidělit explicitně, operací přetypování proměnné (kapitola XII).

type Baze = array [1..10] of Real;
     Ukaz = ^Baze;

var  U    : Ukaz;
     B    : Baze;
     Z    : Char;

function FU (X: Char): Ukaz;
...
U^ a FU (Z)^ jsou proměnné typu Baze
Ptr (50, 50)^ a (@B)^ jsou proměnné nedefinovaného typu
U^[1] až U^[10] jsou prvky pole U^ — proměnné typu Real


PŘÍKLAD: Ve standardním textovém módu obrazovky je zobrazeno 25 řádek po 80 znacích. Každé pozici přitom odpovídá ve videopaměti dvoubytové slovo. Videopaměť je částí operační paměti, začíná na efektivní adrese B8000 (hexadecimálně) a slova jsou v ní řazena po řádkách obrazovky shora dolů a uvnitř řádek po sloupcích zleva doprava. Každé slovo obsahuje v nižším bytu zobrazený znak a ve vyšším bytu jeho barevný atribut (nejnižší čtyři bity atributu definují barvu znaku, další tři barvu jeho pozadí a nejvyšší bit příznak blikání).

Následující procedura PisZnak vypíše na pozici obrazovky danou parametry R (řádka, shora od nuly) a S (sloupec, zleva od nuly) znak Znak s barevným atributem Atr.

{----------------------------------------------------------------------}
procedure PisZnak (R, S: Byte; Znak: Char; Atr: Byte);
{----------------------------------------------------------------------}

 type VideoChar = record                 { struktura slova videopaměti }
                   Symbol  : Char;       { nižší byte                  }
                   Atribut : Byte        { vyšší byte                  }
                  end;

 var  Pozice    : ^VideoChar;            { ukazatel na vybrané slovo   }

begin { PisZnak }
 Pozice := Ptr($B800, 2 * (S + 80 * R)); { adresa - výběr slova        }
 with Pozice^ do                         { zápis do vybraného slova    }
  begin
   Symbol  := Znak;                      { znak                        }
   Atribut := Atr                        { atribut znaku               }
  end
end; { PisZnak }

Určitého zprůhlednění přístupu k jednotlivým slovům ve videopaměti lze dosáhnout použitím dvourozměrného pole jako bázového typu ukazatele Pozice. Hodnota ukazatele pak bude konstantní, bude stále referovat začátek bloku videopaměti — pole — a přístup k jednotlivým slovům bude realizován pomocí indexů:

{----------------------------------------------------------------------}
procedure PisZnak (R, S: Byte; Znak: Char; Atr: Byte);
{----------------------------------------------------------------------}

 type  VideoChar = record                { struktura slova videopaměti }
                    Symbol  : Char;      { nižší byte                  }
                    Atribut : Byte       { vyšší byte                  }
                   end;
       VideoRAM  = array [0..24, 0..79]  { struktura stránky videopam. }
                   of VideoChar;

 const Pozice    : VideoRAM              { ukazatel na stránku         }
                 = Ptr($B800, 0);

begin { PisZnak }
 with Pozice^[R, S] do                   { zápis do vybraného slova    }
  begin
   Symbol  := Znak;                      { znak                        }
   Atribut := Atr                        { atribut znaku               }
  end
end; { PisZnak }

Ukazatelové konstanty   Jazykem je předdeklarována jediná standardní ukazatelová konstanta nil. Pojmenovává ukazatele s nulovou segmentovou i ofsetovou částí reprezentované adresy.

const nil = Ptr (0, 0);

Typem konstanty nil je univerzální ukazatelový typ Pointer (kompatibilní se všemi ukazatelovými typy). Podle konvencí jazyka se hodnota nil používá pro reprezentaci ukazatele, který „nikam neukazuje“ (nereferuje žádnou proměnnou).


POZNÁMKA: Z hlediska syntaxe zdrojového textu není lexikální element nil identifikátorem, nýbrž rezervovaným slovem jazyka, proto se obvykle píše malými písmeny.

Pomocí funkce Ptr s konstantními parametry lze zavádět i uživatelské ukazatelové konstanty (rovněž jejich typem pak bude Pointer):

const VideoRAM = Ptr ($B800, 0);
Ukazatelové typové konstanty   V deklaraci typové konstanty, jejímž typem je ukazatel, je třeba uvést počáteční hodnotu konstanty buď jako identifikátor nějaké (již deklarované) ukazatelové konstanty (například nil) nebo ve tvaru konstantní adresy. Konstantní adresa je aplikace operátoru @ či funkce Addr na již deklarovaný objekt (proměnnou nebo podprogram) nebo volání funkce Ptr s konstantními parametry. Ukazatelové funkce SegOfs lze využít pouze k deklaraci počáteční hodnoty typových konstant, jejichž typem je dvoubytový celočíselný typ (například Word).
type  Ukaz = ^Real;
var   A : Real;

const B : Ukaz = @A;           { korektní deklarace }
      C : Ukaz = Addr (A);
      D : Ukaz = Ptr (10, 20);
      E : Ukaz = nil;
      F : Word = Seg (A);
      G : Word = Ofs (A);

      H = @A;                  { chybné deklarace   }
      I = Seg (A);
      J : Ukaz = Ptr (Seg (A), Ofs (A));

X.2. 

Dynamické proměnné

Všechny deklarované globální proměnné jsou vytvořeny při spuštění programu a existují až do jeho ukončení. Podobně je i doba existence lokálních proměnných vymezena aktivitou bloku, v němž jsou deklarovány. V tomto smyslu jsou všechny deklarované proměnné statické. Turbo Pascal však umožňuje i explicitní zakládání a uvolňování (rušení) dynamických proměnných. Dynamické proměnné tedy nejsou zakládány a uvolňovány na základě své deklarace v určitém bloku, nýbrž na základě požadavku, který může být vznesen kdykoli v průběhu výpočtu programu.

Dynamické proměnné nejsou deklarovány, nemají tudíž přidělen identifikátor. Jsou přístupné pouze pomocí své adresy, kterou je nutno uchovávat v ukazatelové proměnné. Úsek operační paměti, vyhrazený pro alokaci dynamických proměnných, se nazývá heap.

Založení dynamické proměnné   Založení dynamické proměnné je operace, při níž je na heapu rezervován paměťový úsek určité velikosti. Lze ji realizovat voláním některé ze standardních procedur New či GetMem. Při použití New musí být velikost zakládané proměnné známa již v okamžiku překladu programu, kdežto při použití GetMem je požadovaná velikost proměnné vyhodnocena až za běhu programu.

procedure New (var P: ukazatel);

Skutečným parametrem P může být ukazatelová proměnná libovolného typu. Procedura založí dynamickou proměnnou o velikosti bázového typu parametru a ukazatel na ni v parametru vrací. Uvedení univerzálního ukazatele na místě parametru P není chybou, je však založena dynamická proměnná o velikosti 0 bytů.

procedure GetMem (var P: ukazatel; Velikost: Word);

Založí dynamickou proměnnou požadované velikosti a její adresu vrací v parametru P. Velikost je uvedena v bytech a její maximální přípustnou hodnotou je 65 521 (větší bloky dat je nutno uchovávat ve více dynamických proměnných). Skutečným parametrem P je zpravidla proměnná typu Pointer. Uvedení typového ukazatele na místě parametru P není chybou, jeho bázový typ však nemá žádný vliv na velikost založené dynamické proměnné.

Pokud není pro zakládanou dynamickou proměnnou na heapu nalezen volný úsek příslušné velikosti, končí volání procedury New resp. GetMem běhovou chybou programu.

Uvolnění dynamické proměnné   Uvolněním dynamické proměnné se rozumí zrušení rezervace jí obsazeného úseku heapu, který pak může být použit pro alokaci dalších dynamických proměnných. Uvolnění lze realizovat voláním jedné ze standardních procedur DisposeFreeMem. Pokud není příkaz k uvolnění dynamické proměnné explicitně vydán, zůstává příslušný úsek heapu trvale rezervován.

procedure Dispose (var P: ukazatel);

Hodnotou parametru P (libovolného ukazatelového typu) je specifikován ukazatel na uvolňovanou proměnnou a jeho bázovým typem velikost uvolňované proměnné. Procedura totiž předpokládá, že na uvedené adrese se nachází dynamická proměnná bázového typu ukazatele P. Uvedení ukazatele univerzálního sice není chybou, uvolněna je však proměnná o velikosti 0 bytů.

procedure FreeMem (var P: ukazatel; Velikost: Word);

Hodnotou parametru P (libovolného ukazatelového typu) je specifikován ukazatel na uvolňovanou proměnnou a hodnotou parametru Velikost velikost uvolňované proměnné. Velikost bázového typu (v případě uvedení typového ukazatele) nemá na velikost uvolněné proměnné žádný vliv.

Při pokusu o uvolnění neexistující dynamické proměnné končí volání procedur DisposeFreeMem běhovou chybou programu


PŘÍKLAD: Způsob práce s ukazateli a dynamickými proměnnými a důsledky jednotlivých kroků ilustruje následující program.

 type Base = Char; { deklarace bázového typu ukazatele                 }
      Ukaz = ^Base;{ deklarace ukazatelového typu                      }
 var  P, Q : Ukaz; { při spuštění programu jsou založeny dvě statické  }
                   { proměnné typu Ukaz                                }

begin { program }
 New (P);          { na heapu je založena dynamická proměnná typu Base }
                   { a odkaz na ni je vrácen v P                       }
 P^ := 'A';        { definována hodnota dynamické proměnné P^          }
 New (Q);          { založena další dynamická proměnná typu Base       }
                   { a odkaz na ni vrácen v Q                          }
 Q^ := P^;         { definice hodnoty proměnné Q^ hodnotou proměnné P^ }
 Q := P;           { Q přiřazen ukazatel na proměnnou P^ - P i Q       }
                   { referují tutéž dynamickou proměnnou a ukazatel na }
                   { úsek heapu, v němž je alokována dynam. proměnná,  }
                   { původně referovaná proměnnou P, je ztracen - úsek }
                   { zůstane trvale rezervován a nevyužit              }
 Dispose (Q);      { dynamická proměnná Q^ (= P^) je uvolněna          }
 Dispose (P)       { požadavek na uvolnění neexistující dynamické      }
                   { proměnné - program skončí chybou...               }
end. { program }

X.3. 

Řízení heapu

Při zavádění programu do operační paměti je mu operačním systémem určitý její úsek přidělen jako heap. Požadavek na velikost přiděleného heapu lze (společně s požadavkem na velikost přiděleného zásobníku) ve zdrojovém textu programu specifikovat pomocí direktivy kompilátoru (viz požadavek na přidělení paměti).

Základní údaje o umístění a velikosti přiděleného heapu a o jeho zaplnění obsahují standardní proměnné HeapOrg, HeapEndHeapPtr.

const HeapOrg: Pointer = nil;
      HeapEnd: Pointer = nil;
      HeapPtr: Pointer = nil;

Po zavedení programu do operační paměti je do proměnné HeapOrg i do proměnné HeapPtr vložena adresa dna (počátku) heapu, do proměnné HeapEnd pak adresa konce heapu. Při postupné alokaci a uvolňování dynamických proměnných v průběhu výpočtu programu je hodnota proměnné HeapPtr automaticky aktualizována tak, že vždy ukazuje na vrchol obsazené části heapu (nové dynamické proměnné jsou postupně alokovány od dna heapu směrem nahoru).

Pokud jsou všechny alokované dynamické proměnné uvolňovány v přesně opačném pořadí, než v jakém byly založeny, je obsazená část heapu trvale souvislá a tudíž adresami dna a vrcholu plně definovaná. V takovém případě lze k hromadnému uvolnění několika naposled založených dynamických proměnných s výhodou použít procedury MarkRelease (namísto postupného uvolňování jednotlivých proměnných procedurou Dispose či FreeMem).

procedure Mark (var P: Pointer);

Procedura Mark uloží do parametru P aktuální hodnotu vrcholu heapu pro pozdější využití procedurou Release.

procedure Release (var P: Pointer);

Procedura Release vloží do ukazatele na vrchol heapu (HeapPtr) hodnotu parametru P, získanou dříve voláním procedury Mark. Všechny mezitím alokované dynamické proměnné se ocitnou nad novým vrcholem heapu — jsou uvolněny. Volání Release(HeapOrg) uvolní celý heap.

Procedurami DisposeFreeMem lze uvolnit kteroukoliv dynamickou proměnnou, tedy nejen proměnnou na heapu nejvýše položenou. Tím ovšem dochází k fragmentaci heapu, neboť v jeho obsazené části vznikají tzv. díry — neobsazené úseky. Seznam všech děr si program automaticky udržuje na konci heapu a kontroluje jej při zakládání nových dynamických proměnných, které jsou pak do děr přednostně alokovány.

Obrázek 48: Organizace heapu

Procedura Release kromě nastavení hodnoty proměnné HeapPtr také „vyčistí“ seznam děr, tj. inicializuje jej jako prázdný. Pokud pod novým vrcholem heapu nějaké díry existují, chovají se dále jako obsazené. Při společném používání procedur DisposeFreeMem s procedurami MarkRelease je třeba vzít toto nebezpečí na vědomí a postupovat tak, aby takové nezjistitelné a dále nevyužitelné úseky na heapu nemohly vzniknout.

Pro testování velikosti volné paměti na heapu jsou implementovány funkce MemAvailMaxAvail. Jejich použitím před požadavkem na založení dynamické proměnné se lze vyhnout běhové chybě, která by nastala v případě jeho neuspokojení.


POZNÁMKA: Ke stejné chybě může dojít i v okamžiku uvolňování dynamické proměnné. Příčinou je růst seznamu děr.

function MemAvail: LongInt;
function MaxAvail: LongInt;

Funkce MemAvail vrací celkový počet volných bytů na heapu (včetně děr mezi jeho dnem a vrcholem), funkce MaxAvail počet bytů největšího souvislého volného úseku heapu.