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.
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;
asm
mov ax,[bp+4]
end;
begin
writeln(test(1,2,3,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říkazů, 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
implementation
begin
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
unit Test1;
interface
implementation
uses Test2;
begin
writeln('Pozdrav z Test1');
end.
Test2
unit Test1;
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);
Function MensiI(a,b:integer):integer;inline($58/$5b/$3b/$c3/$7e/$01/$93);
Function KeyPressed:boolean; inline($b4/$0b/$cd/$21);
Function ReadKey:char; inline($b4/$08/$cd/$21);
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;
var s:string;
a:byte;
begin
s:='Tak podle nasi miry, vypili jsme malo.';
a:=DelkaRetezce(s);
a:=DelkaRetezce('Tak podle nasi miry, vypili jsme malo.');
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;
var s:string;
a:byte;
begin
s:='Tak podle nasi miry, vypili jsme malo.';
a:=DelkaRetezce(s);
a:=DelkaRetezce('Tak podle nasi miry, vypili jsme malo.');
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;
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 napoprvé 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);
writeln(s:15+Length(s)-1);
writeln(s:15:5);
Veliká škoda je, že
nejsou možné konstrukce typu:
var s,t:string;
...
s:=t:10;
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]);
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 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í!
Datum: 29.5.2008 15:47
Od: imcold
Titulek:
Možno by nebolo na škodu poznamenať, že inline má vo Freepascale trochu inú funkciu, keď sa už FPC spomína aj v inej časti textu.
Datum: 28.2.2009 12:06
Od: Laaca
Titulek:
INLINE má ve Freepascalu stejnou funkci, tedy opět jde o makro, ale už se nemusí zapisovat ve strojovém kódu. Může se napsat normálně v pascalu.