Debugging

Tato lekce je jednou z nejdůležitějších - pojednává o tom, jak odstranit chyby (česky se tomuto procesu říká ladění). 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

Čí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:

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:

  1. 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)

  2. 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:

  1. 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).
  2. 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.
  3. 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...)
  4. 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í:

  1. Zapnout všechny omezující volby v Menu Options / Compiler / ...
  2. Projít si znova celý program
  3. Odstranit nepotřebné bloky programu a soustředit se na hledání chyby
  4. Využít direktiv překladače
  5. 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 -

  1. 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
  2. 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.
  3. 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
  4. 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} 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í.

  1. Spuštění programu - Ctrl-F9 - Občas odhalíme, že je někde chyba
  2. 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.
  3. 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.

Pro dnešek je to vše.