int21h

Řetězcové typy v pascalu

Tradičním typem pro uchovávání řetězců je v pascalu typ string. Je to velmi mocný a příjemný rys jazyka pascal, který nám programátoři v jiných jazycích jako C nebo C++ jen tiše závidí. Vnitřně jde o pole znaků velikosti [0..n]
N je implicitně 255, ale může být menší. V nultém bajtu (znaku) je uložena délka řetězce. Maximální velikost bajtu je 255, takže z toho vyplývá i maximální možná délka řezězce - 255 znaků.
Podívejme se rychle na deklarace:
var s:string;
    t:string[30];  
Proměnná s je vnitřně polem [0..255] a proměnná t polem [0..30]
Proto do proměnné t nemůžeme uložit řetězec delší než 30 znaků. Na druhou stranu tím ušetříme cennou paměť.
Co se ovšem stane, jestliže máme proceduru, která očekává řetězcový argument s určenou délkou?
Podívejme se tento kód:
{$V+}
type s30 = string[30];
procedure pracuj(var s:s30);
begin
end;


var s:string;
    t:string[30];
    v:string[40];
begin
pracuj(s);  {ne}
pracuj(t);  {ano}
pracuj(v);  {ne}
end.  
Chování překladače závisí na nastavení kompilačních direktiv. Jestliže máme zapnuté přísné hlídání řetězců(neboli {$V+}), tak nám pascal povolí jen Pracuj(t);
Pokud je toto chování vypnuto, tak vezme všechny tři varianty.

Až na tento zádrhel je typ string jedním z nejkrásnějších prvků jazyka pascal.
Nicméně omezení na 255 znaků může být v některých případech problém. Další nepříjemnost spočívá v tom, že funkce DOSu i Windows používají jako argumenty tzv. nulou ukončené řetězce (ASCIIZ řetězce). Proto byl do pascalu zaveden typ PChar.
PChar je ve skutečnosti ukazatelem na buffer znaků. PChar má tedy ve skutečnosti zakuklený typ pointer. Ukazuje tedy na buffer (např. pole), který je naplněn znaky. Na rozdíl od stringu, kde je délka "pole" určena jeho prvním bajtem, není délka PCharového bufferu explicitně určena, ale znak ASCII 0 má funkci ukončovače. Jestliže chce překladač určit délku řetězce string, podívá se na jeho první bajt. Jestli že chce určit délku typu PChar, přejde na adresu zapsanou v PCharu a projíždí buffer bajt po bajtu tak dlouho, než narazí na ASCII kód 0. Je zřejmé, jak je to oproti stringům pomalé.
Jestliže je PChar převlečený pointer, tak stejně jako u pointerů tu vyvstávají problémy s platností ukazatelů, deklaracemi paměti atd. Prostě jako v céčku :-)
Pro pohodlnější práci s PChary dělá v jejich případě pascal jisté ústupky v typové kontrole a syntaxi. Proto je možná následující deklarace:
var p:pchar;
begin
p:='Ahoj svete';
writeln(p);
end.  
Jak to, že jsme nemuseli použít proceduru GetMem a alokovat paměť? Inu, protože při přiřazování konstant se pcharový buffer vytvoří v zásobníku. Jestliže ale chceme přiřadit proměnnou, tak si musíme poradit sami.
Další rozšíření syntaxe je možnost indexace jako by šlo o normální pole (nebo string). Proto můžeme provést toto:
var p:pchar;
    c:char;
begin
p:='Ahoj svete';
c:=p[1];  
Vidíte? Vidíte to? Ačkoliv nikde nedeklaruju žádné pole, tak sem píšu index pozice. A potom - nikam nepíšu stříšku! Ti co pascal dobře znají si teď možná klepou na čelo a říkají si: "Ten Laaca je ale lemro, to ví přece každý!" Možná, ale je na to potřeba důkladně upozornit, protože se jedná o dosti výraznou inkonzistenci jazyka pascal.
V tomto zdrojáčku jsem použil přiřazení c:=p[1];
Jakou hodnotu má po přiřazení podle vás proměnná c?
Kdo hádal, že 'A' má smůlu. Správná odpověď je 'h'
Pozice v PCharu se totiž čísluje od nuly!
Ve stringu samozřejmě od jedničky

Situaci ještě komplikuje fakt, že Freepascal má pro typ PChar další rozšíření a dokonce FP 2.0.2 má další rozšíření syntaxe oproti FP 1.0.10
v této tabulce je ukázáno, co které překladače povolí:

var s:string; p:pchar; c:char; i:integer;

 TPFP1FP2
p:=s;nenene
s:=p;neano*ano*
c:=p[0];anoanoano
writeln(p);anoanoano
l:=length(p);neneano**

* Při zapnuté direktivě {$MODE TP} to bude ale NE
** I při direktivě {$MODE TP} to bude stále ANO

Zastavme se u přiřazení p:=s;
žádný z překladačů ho nepovoluje, ale je jasné, že se bez něho obejít prostě nedá. Co s tím? Není to zase tak těžké, podívejte se na tento příklad:
Procedure Vypis(s:string);
var p:char;
begin
s:=s+#0;
p:=@s;
inc(p);
writeln(p);
end;  
Vše je myslím jasné.
1) PChar potřebuje ukončovací znak ASCII 0, proto ho do řetězce přidáme.
2) PChar je vlastně ukazatel, takže mu dáme ukazovat na s.
3) První bajt s je informace o délce, takže ho musíme přeskočit.

Verze TP/BP 7.0 má standardní knihovnu strings, kde je deklarováno mnoho funkcí pro práci s řetězci typu PChar. Mimo jiné jsou tam převodní procedury
Function StrPcopy(cil:pchar; zdroj:string):pchar - zkopíruje string do pcharu. Je ale na programátorovi, aby si předpřipravil dostatečně velký buffer.
StrPas(p:pchar):string - zkopíruje pchar do stringu. Tedy to, co FP dělá sám od sebe.

Pro úplnost ještě dodejme, že Freepascal (FP 1.0.10 i FP 2.0.2) definuje dva nové typy řetězců: ansistring a widestring
Ansistring je podivný hybrid, který se zvnějšku chová stejně jako string (ve všech deklaracích jsou vzájemně zaměnitelné), ale vnitřně jde o velmi složitý typ, který ovšem vychází z typu PChar
Widestring je specializovaný řetězcový typ pro texty ve formátu unicode. Jeden znak tedy není jeden bajt, ale dva bajty. Bohužel vám o tomto zajímavém datovém typu nemůžu říct nic bližšího, protože jsem s ním zatím nepracoval.

Ale honem zpět k PCharům.
Jistá těžkopádnost při jejich používání, především nutnost alokací paměti, mě inspirovala k tomu, napsat objekt, který tyto nedostatky odstraní.
Následující jednotka definuje objekt TPChar, který se automaticky stará o alokaci a dealokaci paměti. Dále si neustále udržuje informaci o velikosti a umožňuje snadné vkládání řetězců typu string.
Nevolá unit strings ani žádnou další jednotku, ale právě prostředky jednotky strings se dá pohodlně rozšiřovat.

Jednotku lze použít se všemi zmíněnými překladači, ačkoliv největší přínos bude mít v TP. Na dvou místech si můžete povšimnout štěpení kódu podle typu překladače. Jedno štěpení je kvůli proceduře ReAllocMem, která v TP oproti FP chybí a druhé je funkce Pchardelka, která má dokonce tři implementace:
1) TP 16-bit assembler
2) FP1 32-bit assembler
3) FP2 pascalovský kód (tady jsou jisté obtíže v kompatibilitě assemblerových pasáží s FP1, proto píšu pro FP2 vlastní variantu)

unit pcharobj;
interface
type
PPchar = ^TPChar;
TPChar = object
   p:pchar;          {buffer znaku}
   d:longint;        {delka retezce}
   _dd:longint;      {delka alokovaneho bufferu (interni promenna)}
   Constructor Init;
   Function VratZnak(n:longint):char;
   Procedure VlozP(s:pchar;poz:longint);    {jako Insert}
   Procedure VlozS(s:string;poz:longint);   { to same   }
   Function Delka:longint;                  {jako Length}
   Procedure Vyjmi(poz,l:longint);          {j. Delete  }
   Function Dej(poz,l:longint):string;      {j. Copy    }
   Destructor Done;
   end;


implementation
const TPCHAR_GRANULARITA = 16;
{Klicovy prvek objektu. Aby se po pridani kazdeho znaku nemusela prealokovavat
pamet, tak se alokuje vzdy minimalne 16 bajtu a dalsi se pridava az podle potreby
granularita by mela byt nasobek 16, protoze DOS ma interne prave 16 bajtovou granularitu
pameti. Tedy prikaz "GetMem(p,1)" alokuje ve skutecnosti 16 bajtu}


{$IFDEF FPC}
   {$IFDEF VER2}
      Function PcharDelka(p:pchar):longint;
      begin
      PcharDelka:=length(p);
      end;
      {$ELSE}
      Function PcharDelka(p:pchar):longint;assembler;
      asm
      xor eax,eax
      mov esi,p
      @znova:
      cmp byte [esi],0
      je @konec
      inc esi
      inc eax
      jmp @znova
      @konec:
      end;
      {$ENDIF}
   {$ELSE}
   Function PcharDelka(p:pchar):longint;assembler;
   asm
   push ds     {registry BP,SP,SS,DS,CS musi byt na konci stejne jako na zacatku}
   xor ax,ax
   xor dx,dx
   lds si,p
   @znova:
   cmp byte [ds:si],0
   je @konec
   inc si
   inc ax
   jmp @znova
   @konec:
   pop ds      {obnova}
   end;
   {$ENDIF}


Procedure Realokace(var p:pchar;n1,n2:longint);
var q:pointer;
    w:word;
begin
{$IFDEF FPC}
   ReAllocMem(p,n2);
   {$ELSE}
   GetMem(q,n2);
   if n2>n1 then w:=n1 else w:=n2;
   Move(p^,q^,w);
   FreeMem(p,n1);
   p:=q;
   {$ENDIF}
end;


Constructor TPChar.Init;
begin
d:=1;
_dd:=TPCHAR_GRANULARITA;
GetMem(p,_dd);
p[0]:=0;
end;


Function TPChar.VratZnak(n:longint):char;
begin
VratZnak:=p[n-1];
end;


Function TPChar.Delka:longint;
begin
Delka:=PcharDelka(p);
end;


Function TPChar.Dej(poz,l:longint):string;
var s:string;
begin
s[0]:=char(l);
move(p[poz-1],s[1],l);
Dej:=s;
end;


Procedure TPChar.VlozP(s:pchar;poz:longint);
var t1,t2,np:Pchar;
    o_dd,n:longint;
begin
dec(poz);              {pozici budu cislovat od 1, tak jako u typu string}
n:=PcharDelka(s);
if n=0 then Exit;      {diskutabilni. snad je to OK}
if d+n>_dd then       {bude treba provest realokaci pameti}
   begin
   inc(d,n);
   o_dd:=_dd;
   _dd:=(d div TPCHAR_GRANULARITA+1)*TPCHAR_GRANULARITA;
   GetMem(np,_dd);      {pripravim novy buffer}
   t1:=np;
   t2:=p;
   inc(t1,poz);
   inc(t2,poz);
   Move(p^,np^,poz);
   Move(s^,t1^,n);
   inc(t1,n);
   poz:=d-n-poz-1;
   Move(t2^,t1^,poz);
   inc(t1,poz);
   t1^:=#0;            {pri kopirovani 0 bajtovych bloku se nezkopiruje zarazka, tak ji doplnim rucne}
   FreeMem(p,o_dd);
   p:=np;
   end
   else begin          {realokace pameti nebude treba}
   t1:=p;
   t2:=p;
   inc(t1,poz);
   inc(t2,poz+n);
   Move(t1^,t2^,d-poz); {posun textu vpravo od vlozeneho}
   Move(s^,t1^,n);        {a vlozeni S}
   inc(d,n);
   end;
end;


Procedure TPChar.VlozS(s:string;poz:longint);
var t:Pchar;
begin
s:=s+#0;
t:=@s;
inc(t);
VlozP(t,poz);
end;


Procedure TPChar.Vyjmi(poz,l:longint);
var t1,t2:Pchar;
    np:longint;
begin
dec(poz);
t1:=p;inc(t1,poz);
t2:=p;inc(t2,poz+l);
Move(t2^,t1^,d-poz-1);
dec(d,l);
np:=(d div TPCHAR_GRANULARITA+1)*TPCHAR_GRANULARITA;
if np<>_dd then Realokace(p,_dd,np);  {P uz ale muze ukazovat jinam (obsah P se muze zmenit)}
_dd:=np;
end;


Destructor TPChar.Done;
begin
FreeMem(p,_dd);
_dd:=0;
d:=0;
end;


end.  



Příklad na vyzkoušení:
uses PCharObj;
var s,t:string;
    p:TPChar;
begin
s:='Ahoj svete, podivej na moji automatickou realokaci pameti!';
p.init;
p.VlozS(s,1);
writeln(p.p);
writeln(p.dej(1,4));
writeln('Delka zpravy: ',s.d,' znaku.');
readln;
p.done;
end.  
2006-12-06 | Laaca
Reklamy:
„Žádný člověk není tak bohatý, aby mohl koupit svoji minulost.“ Oscar Wilde