|
Debugging
Tato lekce je jednou z nejdůležitějších - pojednává o tom, jak odstranit chyby (česky se tomuto procesu říká lazení). Pokud jste psali nějaké vlastní programy, víte, že odhalit chybu je občas mnohem těžší, než napsat celý program znova. Pokud vám taková zkušenost chybí, nezoufejte, je to jen otázka času.
Bug = brouk Debugging = odstraňování brouků z programu. (Jenomže Čechům se do programů spíše vloudí mouchy :( )
Obsah:
Prevence Chyba - Odhalení chyby - Odstranění chyby - Přepsání chyby - Nalezení chyby Závěrečné cvičení
Prevence
Čím méně chyb budeme dělat, tím lépe. Abychom jejich počet snížili na minimum a usnadnili si jejich případné hledání, je dobré držet se následujících zásad:
Vím, co programuji (k počítači sednu, až když vím, jak chci program napsat) Nejsem-li si zcela jist, jak danou věc naprogramovat, použiji metody shora dolů, u krátkých, ale složitých algoritmů si raději vezmu tužku a papír Snažím se o přehlednost: 1) Používáme vždy smysluplné názvy (krátké, ale výstižné) 2) Pocet_Mrtvych_Kachen je lepší než PMK; 3) Dbáme, abychom psali velká a malá písmena vždy stejně - ne tedy jednou write a podruhé Write (o lidech, co píší wRiTE ani nemluvě) - Nepoužíváme názvy jinde definovaných Identifikátorů (není-li to nezbytně nutné) - Je hezké mít var Sin : integer; Ale co když pak potřebuji funkci Sinus?! - Využíváme základních programátorských konvencí (názvy ukazatelů začínají písmenem P, u výčtového typu uvádíme zkratku daného typu, identifikátory typů a tříd začínají písmenem T... - Prázdnými řádky oddělujeme logicky nesouvislé celky. - Každý příkaz a stejně tak i slova begin a end píšeme na samostatný řádek. - Příkaz se nikdy nedělí uprostřed výrazu. - Je-li příkaz rozdělen na více řádků, každý další řádek o jedna(dvě) odsazujeme. - Každý vnořený blok je o 3 znaky posunut doprava. U delších programů pak stačí posunout blok o 2 či dokonce o jeden znak. - end píšeme přesně pod příslušný begin a to dříve, než doplníme ostatní příkazy - Používáme pouze komentářů ve složených závorkách: {Toto je spravny komentar} - Kdykoliv máme pocit, že by někomu nezasvěcenému něco nemuselo být jasné, raději to okomentujeme (proč jsme použili právě tohle) - Komentáři pojmenováváme dílčí logické celky ( {Uvod}, {Pri necinnosti uzivatele}) - Program členíme do menších celků pomocí unit, procedur a funkcí - Hlavičku znatelně oddělujeme od deklarací a definicí a ty zase od samotného těla programu
4) Používáme pouze nám dobře známých příkazů 5) Příkazy se snažíme psát co v nejlogičtějším pořadí (mám-li možnost výběru) 6) Snažíme se používat konstanty a výčtový typ 7) Proměnné definujeme až v jednotlivých funkcích, v hlavním programu jen výjimečně Budete-li dodržovat veškeré zde uvedené zásady, vloudí se do vašeho programu chyba jen výjimečně.
Chyba
Občas se přece jen stane, že se nějaká ta chybička vloudí... Chyby dělíme na dvě velké skupiny:
Chyby syntaktické (výrazové) Chyby v zápisu - napíšeme něco, co není překladači jasné (např. Writel('Ahoj');) Tyto chyby nejsou moc zákeřné - jedná se většinou o překlepy a zapomenuté středníky, na které nás upozorní překladač a my je můžeme v klidu opravit. Problém nastává, pokud si nejsme jisti, jak překlep opravit (Máme dvě funkce MNT a NMT a napsali jsme MMT...
Nejzáludnější chybou je vynechání endu. Někdy je totiž velice nesnadné rozhodnout, kam takový end patří (před tenhle příkaz nebo až za něj, nebo že by až sem a tady by se taky tak pěkně vyjímal... Potenciálně ho můžeme plácnout kamkoliv do těla programu - což ovšem znamená většinou okolo desetitisíců řádek) Této chybě je lépe se vyhnout - psát end hned za beginem a pak teprve mezi ně dopsat příkazy... Dále je vhodné členit program do menších celků (víme-li, že jsme end zapomněli napsat v konkrétní patnáctiřádkové proceduře, je to lepší než uděláme-li to samé v 100000-řádkovém programu)
Chyby sémantické (obsahové) Chyby ve významu - napíšeme něco, co dělá něco jiného, než jsme chtěli - sem patří např. středník za do, Napsání jiného čísla, špatně zapsaný řetězec.... Ale i různé špatně vymyšlené konstrukce, nerespektování indexování polí... Během psaní programu je neustále dobré si ho spouštět a kontrolovat, zda pracuje tak, jak má. Odstraníme tak všechny syntaktické chyby a i některé chyby sémantické. Později pak máme téměř jistotu, že jsme chybu neudělali v té části programu, kterou jsme několikrát zkontrolovali.
Odhalování chyb
K odhalení syntaktické chyby stačí pokusit se program zkompilovat. K tomu abychom odhalil chybu sémantickou, nestačí občas ani roky testování. Všeobecný postup je takovýto - nejprve se pokusíme náš program spustit. Hlásí-li program syntaktickou chybu, zamyslíme se nad tím, jak ji vhodně opravit, opravíme ji a zkusíme program spustit znova.
Sémantické chyby: nastane-li v jejich důsledku v programu fatální chyba, překladač nám ji ohlásí a sdělí nám, k čemu vlastně došlo (chyb rozeznáváme docela hodně - od dělení nulou po zápis do neotevřeného souboru). Mnohem horší jsou chyby, které se nijak katastrofálně neprojevují - počítáme-li obvod kruhu jako 2*4.14*r, počítači to vadit nebude, ale uživatel bude nadávat na nesmyslné výsledky. Další skupinou sémantických chyb jsou takové chyby, které za určitých okolností vedou k vyvolání nějaké katastrofické chyby (program na dělení dvou čísel, zadá-li uživatel jako druhé číslo nulu...). Na takovéto chyby se dá přijít pouze dlouhým testováním. Jednodušší ovšem je, pokud si během psaní programu děláme poznámky, které kombinace podmínek jsou zakázané a tyto nějak z programu vyloučíme (= napíše uživateli, že druhé číslo nesmí být nula, popř. to otestujete v nějaké podmínce...) Nejhorší skupinou jsou pak chyby, které za určitých okolností vedou k špatnému výsledku programu, nezpůsobí však žádnou katastrofickou chybu. Nejčastější chybou této skupiny je porovnávání reálných čísel. Vlivem zaokrouhlování se velice často stane, že výsledek se trošku liší od našeho očekávání... Tedy např.. if Sqrt(2731)*Sqrt(2731)-2731=0 then writeln('Je to nula'); rozhodně nic nevypíše. V praxi se to obchází tak, že se napíše Abs(a) < hodně malé číslo. V našem příkladě tedy například: (if Abs(Sqrt(2731)*Sqrt(2731)-2731)<0.00001 then writeln('Je to nula'); Obvykle se Abs rozepíše do dvou podmínek: (Sqrt(2731)*Sqrt(2731)-2731<0.00001) and (Sqrt(2731)*Sqrt(2731)-2731>-0.00001) then writeln('Je to nula'); Dává to sice o něco rychlejší kód, ale je to nepřehledné... Rozhodněte se sami...
Odstraňování chyb
Je to jednoduché, stačí chybu najít a přepsat. Přepsat chybu bychom již měli umět. Horší je chybu najít.
Hledání chyb
Nejjednodušší je nalézt chyby syntaktické. Ty totiž ohlásí již překladač. (Zapomenutý end tvoří výjimku, ohlásí se až na samém konci programu.)
K nalezení chyb sémantických je nutno použít drsnějších nástrojů. Zamyslete se nejprve nad tím, co by danou chybu mohlo způsobovat a v které části programu se daná chyba může vyskytovat (často je to značně daleko od jejího prvního projevu). Pozorně si prohlédněte podezřelá místa a zkontrolujte, zda jste na nic nezapomněli.
Že jste chybu nenašli? Máte nyní několik možností:
Zapnout všechny omezující volby v Menu Options / Compiler / ... Projít si znova celý program Odstranit nepotřebné bloky programu a soustředit se na hledání chyby Využít direktiv překladače Krokování Ad 1) Zapneme omezující volby a doufáme, že se chyby sémantické stanou pod přísnějším pohledem překladače syntaktickými. (Nejde použít v případě, že vědomě využíváme některých výhod volnější syntaxe.)
Ad 2) Procházíme celý kód od začátku a každý příkaz zdůvodňujeme - jestli dělá skutečně to, co má.
Ad 3) Celé bloky programu lze vyřadit použitím těchto závorek (* Blok programu.... *) Dá se to ale udělat pouze tehdy, pokud jste tyto závorky nepoužívali k psaní komentářů. (A podobně pokud používáte ke komentování pouze (* a *), můžete celé úseky programu vyřadit z činnosti pomocí složených závorek.) Vyřazování celých částí programu se používá několika způsoby - vyřadíme již zkontrolovanou část programu, či část, kde se chyba nemůže vyskytnout a získáme tak kratší kód, ve kterém se snadněji orientujeme při použití dalších metod vyřadíme příkaz, u kterého se chyba projevila, objeví-li se i někde jinde, máme jistotu, že chyba předcházela vyřazenému bloku (někdy s tím zamíchá pořadí, v jakém jsme definovali procedury). V tomto případě musíme mít v programu příkazy, u kterých se může chyba projevit projevit. Ty lze do programu dodatečně přidat a pak je z něj odstranit) Pokud s vyřazením příkazu chyba zmizí, máme jistotu, že se vyskytuje pouze ve vyřazeném bloku. vyřadíme místo, kde by se chyba mohla nacházet - projevuje-li se dál, tak tam není, přestane-li se projevovat, nejspíše je někde ve vyřazeném úseku vyřadíme náhodné místo a sledujeme, co se bude dít... Poslední zoufalý pokus... Ad 4) IDE je k nám milostivo a umožňuje využívat tzv. podmíněný překlad - části programu, které se přeloží pouze tehdy, je-li splněna daná podmínka {$DEFINE Jmeno} Oznámí překladači, že jsme definovali symbol Jmeno {$UNDEF Jmeno} Oznámí překladači, že jsme zrušili definici symbolu Jmeno {$IFDEF Jmeno} Následující příkazy až k {$ENDIF Jmeno} se přeloží pouze v případě, že je definován symbol Jmeno {$IFNDEF Jmeno} Následující příkazy se provedou pouze v případě, že Jmeno není definován. {$IFOPT Switch} Následující příkazy se vykonají pouze tehdy, když má daný přepínač danou hodnotu {$IFOPT X+} uses Strings {$ENDIF} {$ELSE} Následující příkazy se vykonají, nebyla-li splněna předchozí podmínka (IFDEF,IFNDEF, IFOPT)
Nejčastěji se podmíněného překladu používá tak, že definujeme konstantu DEBUG a v jednotlivých procedurách si podmíněně necháme vypsat obsah všech důležitých proměnných (a vůbec, občas je dobré i ohlásit, v jakém stádiu se výpočet nachází)... Poté konstantu DEBUG zrušíme a ve výsledném .EXE není nic poznat.
Ad 5) Krokování je velice užitečné, dělí se na několik různých úrovní. Spuštění programu - Ctrl-F9 - Občas odhalíme, že je někde chyba Krokování přes F8 - Provádí program řádek po řádku. Funkce a procedury bere jak jeden příkaz. - Takto lze zjistit, kde se chyba projevuje. Krokování do - F7. Aby šla použít, musí být zaškrtnuta v Options všechna políčka týkající se Debuggeru. Provádí program řádek po řádku, ale skočí i do těla procedur... Můžeme zjistit, kde přesně se daná chyba projevuje Občas se nám hodí zkratky Ctrl-F2 (resetuje program, takže ten začne pak znovu od začátku) F4 - Provede program a zastaví se na řádce, na které je kurzor. Od této pozice lze pak krokovat
Také můžeme využívat možností nabídky Debug - Watch. Ta nám umožní sledovat hodnoty zadaných proměnných, Evaluate/Modify - Ctrl-F4 nám dává možnost zobrazené hodnoty i měnit. Output -zobrazí co se v daném momentě nachází na obrazovce programu.
Nepomáhá-li to, necháme problém několik hodin či dní uležet a znovu zkusíme debugging a nezabere-li to, můžeme začít psát program pěkně od začátku...
To by bylo vše. Pište a pište vlastní programy a nebudete-li si s nějakou chybou vědět rady, pročtěte si znova tuto lekci. DCV: Odhalte všechny chyby v tomto kratičkém programu :
program PrikladNaSeznam; {Program vytvoří oboustranně zřetězený seznam a umožní jeho úpravy)} type PSeznam = ^TSeznam; TSeznam = record Jmeno : string; Vek : Byte; Predchozi : PSeznam; Dalsi : PSeznam; end; var Hlavicka,Soucasny : PSeznam; Seznam : TSeznam; ZJmeno : string; ZVek : Byte, r : char;
procedure Inicializace; {Vytvoří si jakési držadlo} begin New(Hlavicka); {Vytvoříme novou dynamickou proměnnou, na kterou bude ukazovat Hlavicka} Hlavicka^.Dalsi:=Hlavicka; {Nic dalšího zatím není, tak ať ukazuje na sebe - kruhový seznam} Hlavicka^.Predchozi:=Hlavicka; Hlavicka^.Jmeno:='nikdo'; Hlavicka^.Vek:=0; {Tím bychom měli hotové jakési držadlo} Soucasny:=Hlavicka; {na které teď ukazuje Soucasny} end;
procedure Zadej; {Vytvoří oboustranně zřetězený kruhový seznam} begin repeat Write('Jméno : '); Readln(ZJmeno); if ZJmeno <> '' then begin Write('Věk : '); Readln(ZVek);
New(Soucasny^.Dalsi); {Nová dynamická proměnná} Soucasny^.Predchozi:=Soucasny; {Ukazatel Predchozi nové proměnné je nastaven na současnou hodnotu Soucasny} Soucasny:=Soucasny^.Dalsi; {Ukazatel současný přesměrujeme na novou proměnnou} Soucasny^.Jmeno:=ZJmeno; Soucasny^.Vek:=ZVek; Soucasny^.Dalsi:=Hlavicka; {Ať se nám kruh neporuší} Writeln; end; until ZJmeno=''; end;
procedure Vypis; {Vypíše celý seznam} begin Writeln('Celý seznam :'); Soucasny:=Havicka; repeat Soucasny:=Soucasny^.Dalsi; Writeln(Soucasny^.Jmeno:40,Soucasny^.Vek:20); until Soucasny^.Dalsi=Hlavicka; Writeln('To je vše'); end;
procedure Zmen; {Vypíše všechny prvky a u každého se zeptá na možnost změny} var c:char; begin Writeln('Celý seznam :'); Soucasny:=Hlavicka; repeat Soucasny:=Soucasny^.Dalsi; Writeln(Soucasny^.Jmeno:20,Soucasny^.Vek:20, ' Změnit(A/N):'); Readln(c); c:=UpCase(c); if C='A' then begin Write('Nové jméno :'); Readln(ZJmeno); Write('Nový věk :'); Readln(Vek); Soucasny^.Jmeno:=ZJmeno; Soucasny^.Vek:=ZVek; end; until Soucasny^.Dalsi=Hlavicka; Writeln('To je vše'); end; procedure Odstran; {Odstraní vybrané prvky ze seznamu} var Smaz:PSeznam; c :char; begin Writeln('Celý seznam :'); Soucasny:=Hlavicka; repeat Soucasny:=Soucasny^.Dalsi; Writeln(Soucasny^.Jmeno:40,Soucasny^.Vek:20,'Odstranit(A/N)'); Readln(c); c:=UpCase(c); if C='A' then begin Smaz:=Soucasny; Soucasny^.Predchozi^.Dalsi:=Soucasny^.Dalsi; {Predchozi prvek ukazuje na prvek za mazaným} Smaz^.Dalsi:=Smaz^.Predchozi;{Následující prvek ukazuje před mazaný} Soucasny:=Smaz^.Predchozi; {Soucasny, jako by tam mazaný prvek vůbec nebyl} Dispose(Smaz); {Teprve nyní můžeme proměnnou vymazat} end; until Soucasny^.Dalsi=Hlavicka; Writeln('To je vše'); end; procedure Pridej; {Přidá nový prvek do seznamu - před vybraný prvek} var Novy:PSeznam; c :char; begin Writeln('Celý seznam :'); Soucasny:=Hlavicka; repeat Soucasny:=Soucasny.Dalsi; Writeln(Soucasny^.Jmeno:2,Soucasny^.Vek:20,'Přidat před něj nový(N/N)'); Readln(c); c:=UpCase(c); if C='a' then begin Write('Nové jméno :'); Readln(ZJmeno); Write('Nový věk :); Readln(ZVek); New(Novy); Novy^.Jmeno:=ZJmeno; Novy^.Vek:=ZVek; Novy^.Dalsi:=Soucasny; Novy^.Predchozi:=Soucasny^.Predchozi; Soucasny^.Predchozi:=Novy; Novy^.Predchozi^.Dalsi:=Novy;
end; until Soucasny^.Dalsi=Hlavicka; Writeln('To je vše'); end;
begin Inicializace; repeat writeln('Co chcete dělat : '); writeln; writeln('V - Vytvořit seznam'); writeln('Z - Zobrazit seznam'); writeln('O - Opravit údaje'); writeln('S - Smazat některá data'); writeln('P - Přidat nové prvky'); writeln('K - Končit'); Readln(R); R:=UpCase(R); case R of 'V': Zadej; 'Z': Vypis; 'O': Zmen; 'S': Odstran; 'P': Pridej; end; until R='k'; end.
Je jich tam docela dost... A nezapomeňte všechno řádně otestovat - obzvláště zadávání, poté zobrazení, úpravu hodnot, mazání a přidávání... Nejste-li si jisti u ukazatelů, raději si nakreslete diagramy. Správné řešení se nachází v některé z předchozích lekcí :) Můžete se podívat, jestli jste našli všechny chyby.
Zdroj: http://programar.webpark.cz
|