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:
type s30 = string[30];
procedure pracuj(var s:s30);
begin
end;
var s:string;
t:string[30];
v:string[40];
begin
pracuj(s);
pracuj(t);
pracuj(v);
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;
| TP | FP1 | FP2 |
p:=s; | ne | ne | ne |
s:=p; | ne | ano* | ano* |
c:=p[0]; | ano | ano | ano |
writeln(p); | ano | ano | ano |
l:=length(p); | ne | ne | ano** |
* 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;
d:longint;
_dd:longint;
Constructor Init;
Function VratZnak(n:longint):char;
Procedure VlozP(s:pchar;poz:longint);
Procedure VlozS(s:string;poz:longint);
Function Delka:longint;
Procedure Vyjmi(poz,l:longint);
Function Dej(poz,l:longint):string;
Destructor Done;
end;
implementation
const TPCHAR_GRANULARITA = 16;
Function PcharDelka(p:pchar):longint;
begin
PcharDelka:=length(p);
end;
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;
Function PcharDelka(p:pchar):longint;assembler;
asm
push ds
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
end;
Procedure Realokace(var p:pchar;n1,n2:longint);
var q:pointer;
w:word;
begin
ReAllocMem(p,n2);
GetMem(q,n2);
if n2>n1 then w:=n1 else w:=n2;
Move(p^,q^,w);
FreeMem(p,n1);
p:=q;
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);
n:=PcharDelka(s);
if n=0 then Exit;
if d+n>_dd then
begin
inc(d,n);
o_dd:=_dd;
_dd:=(d div TPCHAR_GRANULARITA+1)*TPCHAR_GRANULARITA;
GetMem(np,_dd);
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;
FreeMem(p,o_dd);
p:=np;
end
else begin
t1:=p;
t2:=p;
inc(t1,poz);
inc(t2,poz+n);
Move(t1^,t2^,d-poz);
Move(s^,t1^,n);
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);
_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.