e-mail    Debatní kniha    Mapa stránek    Hlavní  
 INT21h 
 

Méně známé konstrukce jazyka pascal - část 2.

Pořadí vyhodnocování parametrů
Inicializační sekce jednotek
Inline funkce
Konstantní parametry
Procedura Val
Formátování výstupu
Debugger


Tento článek je myšlený jako opožděné pokračování článku Méně známé konstrukce jazyka pascal 1. Pojmem "opožděné" mám na mysli dva a půl roku :-( Stejně jako v minulé části se budu zabývat klasickým Turbo pascalem, a ne Freepascalem, jak
jsem tehdy naznačoval. Je to k nevíře, ale i v tomto prastarém jazyce/překladači je toho spousta co objevovat. Případné dotazy, prosím, nepište do diskuze pod článek, ale přímo do hlavní debaty na INT21h, či v nejhorším případě do pascalí
diskuze na Programujte.cz

Pořadí vyhodnocování parametrů


Tímto nadpisem mám na mysli pořadí vyhodnocování vstupních parametrů procedur a funkcí. Jako příklad si ukažme třeba obyčejnou proceduru Write.
Write(s1,s2,s3,s4);
Řešíme problém, jestli pascal bude parametry zpracovávat zleva doprava, nebo zprava doleva. Jak to zjistit? Že to nejde? Ale jde. Využijeme toho, že parametry procedury mohou být i vnořené funkce. Takže si připravíme testovací funkci Param
Function Param(s:string):string;
begin
writeln(s);
Param:='';
end;

begin
Write(Param('opice'),Param('ryba'),Param('slon'),Param('vosa'));
end.
Nu? Kdo si tipne, co udělá tento kód? Půvab je v tom, že Write nenapíše vůbec nic, protože vnořené funkce shodně vrátí prázdný řetězec. Nicméně právě ony vypíšou na obrazovku názvy zvířat. Na obrazovce se objeví:
opice
ryba
slon
vosa

Je tedy zřejmé, že pascal zpracovává parametry funkcí zleva doprava, tedy stejně jako člověk. Možná víte, možná nevíte, že Turbo pascal předává parametry přes zásobník. Tudíž hned navrchu zásobníku bude poslední parametr, který se tedy POPne jako první.
Tedy, abychom byly přesní, nejprve se PUSHne obsah registrů DS a DI a až potom parametry. Instrukce Push je v TP schopna ukládat pouze po dvou bajtech, a rovněž víme, že když přidáváme na zásobník,
tak zapisujeme na nižší a nižší adresy (zásobník roste směrem dolů), takže poslední operand je na adrese [BP+4]. Na zdrojáku je to srozumitelnější:
Function Test(a,b,c,d:integer):integer;assembler;
{jeste pres ASM pascal automaticky prida tento kod: PUSH BP;MOV BP,SP}
asm
 mov ax,[bp+4]  {je rovnocene zapisu mov ax,d}
{mov ax,[bp+6]} {je stejne jako mov ax,c}
end;

begin
writeln(test(1,2,3,4));    {napise "4"}
end.

Inicializační sekce jednotek


Jak víme, jednotky, nebo chcete-li unitky, mají sekce interface a implementation. Ve skutečnosti ale mají sekce tři a tou třetí je inicializační sekce. Ve Freepascalu dokonce existuje klíčové slovo initialization. V TP sice chybí, ale tvrzení o
třech sekcích platí i zde. Mezi poslední procedurou sekce implementation a finálním end. totiž může být i begin a celá libovolně dlouhá sekvence příkszů, jako by šlo o hlavní program.
Kdy se ale tento kód provede? Úplně na začátku programu, jakmile se zpracuje příkaz USES.
Máme-li tedy jednotku Test
unit Test1;
interface
{muze byt prazdna}
implementation
{muze byt prazdna}
begin
{treti, tedy inicializacni blok}
writeln('Pozdrav z Test1');
end.
A k tomu testovací program TestPrg
uses Test1;
begin
readln;
end.
Přestože explicitně nevoláme žádnou proceduru kromě Readln, program vypíše text "Pozdrav z Text1". To jsme ale už tak nějak předpokládali. Tento rys funguje i řetězově - tedy, že jednotky se mohou odkazovat na jiné jednotky, které rovněž mohou mít v inicializační
části cokoliv. Pojďme ale ještě o krok dále. Co se asi stane, když je některá jednotka volána několikrát?

Test1
uses Test1;
begin
readln;
end.

Test2
unit Test2;
interface
implementation
begin
writeln('Pozdrav z Test2');
end.

TestPrg
uses Test1,Test2;
begin
readln;
end.
Jednotka Test2 je tedy volána dvakrát - z hlavního programu a z jednotky Test1. Člověk by si myslel, že zpráva "Pozdrav z Text2" se objeví také dvakrát. Ale ono ne! Každá jednotka si totiž pamatuje, jestli už byla inicializována a
inicializační kód se proto provede vždy jen jednou. A to je dobře!

Inline funkce


Tohle je doopravdy velká obskurnita, ale článek má název "Méně známé konstrukce", což tento fígl splňuje :-)
Jde o to, že ne vždy musí všechny procedury či funkce definované v jednotce v sekci interface, že ne vždy musí mít své protějšky v sekci implemetation. Tuto výjimku přestavují inline funkce. Možná znáte příkaz "inline", který
umožňuje zadávat sekvence ve strojovém kódu. Je to takový předchůdce bloku asm end;
Zkrátka, je li kompletně celá procedura či funkce zapsaná pomocí inline, nemusí být už potom zmíněna v sekci implemetation. Příklad.
unit Ukazka;
interface
Function VetsiI(a,b:integer):integer;inline($58/$5b/$3b/$d8/$7e/$01/$93);
{pop ax;pop bx;cmp bx,ax;jle +1;xchg ax,bx}
Function MensiI(a,b:integer):integer;inline($58/$5b/$3b/$c3/$7e/$01/$93);
{pop ax;pop bx;cmp ax,bx;jle +1;xchg ax,bx}

Function  KeyPressed:boolean; inline($b4/$0b/$cd/$21);{mov ah,0bh;int 21h}
Function  ReadKey:char; inline($b4/$08/$cd/$21);{mov ah,8;int 21h}
implementation
end.
Jestli vás zajímá, jak je možné, že inline má tuto výjimku, tak vysvětlení je v tom, že ve skutečnosti o pravé funkce nejde. Jsou to jen jakási makra. V praxi to znamená, že kód inline "funkce" není volán pomocí CALL a RET, ale rovnou do aktuálního bloku kódu se zkopíruje tělo inline funkce. Zkrátka se chová jako doopravdické makro.
Inline funkce jsou dobrý úlet, potíž je ale v tom, že programovat ve strojovém kódu umí skutečně jen nemnozí :-)
No, dělám si srandu, ve skutečnosti to tak těžké není, kód prostě napíšete v assembleru, zkompilujete a podle hexa editoru, či debuggeru vytáhnete odpovídající stroják.

Konstantní parametry


Většina lidí zná parametry funkcí volané přímo a ty, volané přes var Příkladem budiž prodedura GetDir
Procedure GetDir(i:byte; var s:string);
Správně se tyto dva způsoby nazývají parametry předávané hodnotou a parametry předávané odkazem. V prvním případě se vstupní parametr kopíruje, což vede k tomu, že s touto kopií si uvnitř procedury můžeme dělat co chceme, ale
originál zůstane nezměněn. Velkou nevýhodou je pomalost. Pokud jsou takto předávána čísla, o nic nejde, ale pokud jde o dlouhé řetězce, či dokonce o rozsáhlejší pole, může být čas spotřebovaný na kopírování parametrů znát. Ve druhém případě se předá
pouze adresa, tedy odkaz na originál. Je to sice velice rychlé, ale jelikož pracujeme s "originálem", tak změny parametru vykonané uvnitř procedury se přenašejí vně. To je ostatně účel toho, proč je většinou používáme - tedy jako alternativu k funkcím.
Mrzuté je ale to, že z důvodu, že se tento parametr bude patrně měnit, trvá překladač na tom, abychom takovéto procedury volali s parametry uloženými v proměnných, nikoliv přímými hodnoty. Srozumitelně řečeno:
Function DelkaRetezce(var s:string):byte;
begin DelkaRetezce:=Length(s);end;

{Hlavni program}
var s:string;
    a:byte;
begin
s:='Tak podle nasi miry, vypili jsme malo.';
a:=DelkaRetezce(s);                                        {funguje}

a:=DelkaRetezce('Tak podle nasi miry, vypili jsme malo.'); {nefunguje}
end.
Všimněte si, že v tomto příkladu hodnotu S neměním a volání přes VAR využívám jenom abych se vyhnul zdlouhavému kopírování řetězců. Jenže za to platím určitou nepohodlností při používání. Nemohu řetězec dosazovat přímo.
Proto by bylo fajn, kdyby existovala třetí konvence, která by spojovala obě přednosti - předávání odkazem i možnost přímého zadávání.
Nuže, tato možnost existuje - konstantní parametry
Function DelkaRetezce(const s:string):byte;
begin DelkaRetezce:=Length(s);end;

{Hlavni program}
var s:string;
    a:byte;
begin
s:='Tak podle nasi miry, vypili jsme malo.';
a:=DelkaRetezce(s);                                        {funguje}

a:=DelkaRetezce('Tak podle nasi miry, vypili jsme malo.'); {funguje}
end.
Prostě místo var dáte klíčové slovo const. Konstantní parametr ale znamená, že takto předané parametry nelze uvnitř procedury modifikovat. Pokud překladač zjistí, že ji měníte, ohlásí při překladu "Invalid variable reference".
Dobré ne? Já si to myslel. Jenže ještě lepší je, že se to dá ojebat. Překladač sice uhlídá, jestli takovou proměnnou měníte v pascalovském kódu, ale už neuhlídá, když to uděláte v assemblerovém bloku.
Function Delka_a_NaVelka(const s:string):byte;assembler;
asm
   xor bx,bx
   mov al,[si]
   mov bl,al
@znovu:
   cmp bl,0
   jz @konec
   mov cl,[si+bx]
   dec bl
   cmp cl,97
   jl @znovu
   cmp cl,122
   jg @znovu
   sub cl,32
   mov [si+bx+1],cl
   jmp @znovu
@konec:
end;

{Hlavni program}
var s:string;
    a:byte;
begin
s:='Tak podle nasi miry, vypili jsme malo.';
writeln(s);
a:=Delka_a_NaVelka(s);
writeln(s);
readln;
end.
Procedura Delka_a_NaVelka umí zpracovat i parametry zadávané přímo, tak i přes proměnnou. Pokud je parametr zadaný přes proměnnou, tak ho jako bonus převede na velká písmena.

Procedura Val


Tohle je jenom takový krátký tip. Procedura Val je chytřejší, než si mnozí z vás myslí. Víte, že umí převádět desetinná čísla? Čísla zapsaná v exponenciálním tvaru a písmeno "e" může být i malé, i velké? Že zvládne i čísla v šestnáctkové soustavě?
Čísla v šestnáctkové soustavě se zapisují stejně jako kdekoliv jinde v pascalu, tedy pomocí znaku dolaru. Takže pozor, aby ve vašich rutinách uživatel nezadávak hexadecimální čísla v obvyklejší céčkové konvenci.
Mimochodem, je škoda, že pascal neumí zpracovat desetinná šestnáctková čísla. Zvládne jen celočíselná.

Formátování výstupu


Procedury Write a Writeln umí částečně formátovat svůj výstup. Setkáváme se s tím spíše nevědomky při psaní reálných čísel. Kdo někdy zkusil vypsat reálné číslo jednoduše takto: writeln(3.14); tak se napoprv0 asi dost podivil, co
to vylezlo na obrazovku. Číslo se zkrátka vypíše v exponenciálním tvaru. Ještě tuplem to platí pro hodnoty zadané přes proměnné. Pokud tedy chceme nějak "lidsky" vypsat 3.14, musíme zadat writeln(3.14:4:2)
Čísla za dvojtečkami označují parametry formátování. První údaj znamená na kolik znaků roztáhnout vypisovaný text. Druhý kolik desetinných míst se má vypsat. K druhému parametru není co dodat, ale podívejme se ještě trošku na první. Pokud zadáte menší číslo
než šířku textu, tak se nic neděje. Výstupy z writeln(3.14:4:2) a writeln(3.14:1:2) budou totožné. Vypisovaný text má zkrátka čtyři znaky a i když zadáváme šířku jedna, tak se stejně vypíšou všechny čtyři. Pokud ale zadáme větší hodnotu,
tak bude text odsazen. Např. writeln(3.14:10+4:2) způsobí, že text bude odsazen deseti mezerami. Tento fígl se velice hodí třeba pro tisk tabulek. Ještě lepší je to, že se tento formátovací parametr dá použít i u jiných typů než je real.
var s:string;
...
writeln(s:15);             {posledni vypsany znak bude na 15. sloupci}
writeln(s:15+Length(s)-1); {prvni vypsanyu znak bude na 15. sloupci}

writeln(s:15:5);  {CHYBA - druhy parametr lze pouzit jenom na realna cisla!}
Veliká škoda je, že nejsou možné konstrukce typu:
var s,t:string;
...
s:=t:10;        {nefunguje}
Kromě Write a Writeln fungují formátovací parametry už jenom na proceduru Val. Syntaxe je stejná jako u Writeln. To umožňuje sice obskurní, ale přesto způsob, jak v pascalu rychle vytvořit řetězec o N mezerách:
Str(0:n+1,s);dec(s[0]);  {a v S je retezec obsahujici N mezer}

Ještě mocnějším formátovacím prostředkem je procedura FormatStr z jednotky Drivers. Ta umožňuje formátování výstupu v céčkové syntaxi, tedy napřed maska a zvlášť parametry. Podle mě ohavnost, ale pro céčkaři nic jiného neznají a jistě to ocení při
přechodu na pascal :-) Kromě toho je to doopravdy dosti mocný prostředek. Nemá cenu, abych tu rozepisoval syntaxi, je to popsáno v nápovědě. (napiště "FormatStr" (bez uvozovek), držte Ctrl a klepněte na toto slovo pravým tlačítkem myši - objeví se podrobná nápověda)
FormatStr je mimochodem dobrou možností, jak v Turbo pascalu napsat číslo v šestnáctkové soustavě:
uses Drivers;
var a:longint;
    s:string;
begin
write('Zadej cele cislo: ');
readln(a);
FormatStr(s,'V setnactkove soustave to je %x',a);
writeln(s);
readln;
end.

Debugger


V tomto oddíle se nebudeme věnovat dalšímu rysu jazyka pascal, ale velice stručně si popíšeme, jak pracovat s debuggerem.
Turbo pascal má vnitřní debugger a vnější debugger. Ten vnitřní jde použít jenom pro realmódové programy, vnější pro oba druhy. Základním předpokladem pro debuggování (ladění) programu je, zapnout generování ladicí informace. Jděte do "Options" -> "Compiler" a zašrtněte "Debug information" a "Local symbols"
Dále se ujistěte, že v "Options" -> "Debugger" máte zaškrtnuté "Integrated". Teď jděte někam do zdrojáku a zmáčkněte Ctrl-F8. Takto označený řádek se zvýrazní a znamená to, že zde sedí Breakpoint. To znamená, že když prováděný program dorazí na tento řádek, tak zamrzne, ale neukončí se. Objeví se
obrazovka IDE pascalu a my můžeme používat všelijaké funkce z nabídky Debug, především sledování běhu programu a monitoraci a dokonce modifikace obsahu proměnných. Pro breakpointy se dá dokonce nastavit, jestli mají odskok do IDE provést vždy nebo jen při určité podmínce. Např. když máme breakpoint
v cyklu a zajímá nás chování pouze, když řídící proměnná cyklu je N.
Vnitřní debugger toho umí dost, ale neumí disassembláž, a neumí některé další věci a hlavně se nedá použít pro protektové programy. Všechno tohle ale zvládnou vnější debuggery. Pro realmódové programy jde o TD.EXE či TD286.EXE a pro protektové TD32.EXE a TDX.EXE
Pro plnohodnotné používání těchto debuggerů je třeba zapnout ještě jednu volbu, a to "Options" -> Debugger -> Standalone
Pokud bychom to neudělali, zobrazil by debugger jen assemblerovou disassembláž. To sice není od věci, ale v porovnání s interním debuggerem by to bylo trochu málo. Takže zapněte tedy standalone a přeložte program znovu. Pak ho načtěte nějterým z vnějších debuggerů a voilá, vidíte pascalovský zdroják včetně komentářů
Na diskuzních fórech se čas od času objevují dotazy, zda je možné z EXE souboru zrekonstruovat zdroják. Odpovědí je tedy rozhodné ano, ale jen za předpokladu, že program byl přeložen se všemi zmíněnými nastaveními.
Takže, máme zobrazený zdroják a my můžeme ladit stejným způsobem jako s interním debuggerem, ale možnosti jsou rozsálejší. Nejzajímavější je podle mě možnost CPU window, která ukazuje assemblerový překlad procházené procedury. Tohle je nejlepší způsob jak studovat, jakým způsobem pascal překládá různé konstrukce.
Vnější debugger vám dokonce dovolí nejen měnit hodnoty proměnných, ale dokonce i jednotlivé instrukce, a to za běhu programu!
Turbo pascal tedy neznamená jen skvělý programovací jazyk, ale i skvělé vývojové prostředí!


Pascal - hlavní
Překladače
Vlastní články
Převzaté články
Věci na stáhnutí
Odkazy k tématu
BP7 buglist
Chyba Run-time 200

BASIC