Rozdział 9. Wykrywanie błędów w aplikacjach

Adam Boduch

Trzeba sobie uświadomić, że błędy w aplikacjach są — niczym śnieg na biegunie — elementem nieodłącznym. Wielu programistów często z pośpiechu, lecz także z braku wystarczających umiejętności, marginalizuje problem błędów w aplikacjach. Wiele firm, mimo ogromnego zaangażowania setek programistów oraz dużych nakładów finansowych, wciąż nie jest w stanie pozbyć się wszystkich błędów (np. Microsoft), bez przerwy publikując nowe poprawki do swoich produktów. Tylko program zawierający trzy linie kodu źródłowego może być pozbawiony jakichkolwiek błędów, lecz w przypadku skomplikowanych, rozbudowanych aplikacji uniknięcie niedoskonałości nie jest możliwe. Dzieje się tak dlatego, że człowiek jest tylko człowiekiem i po prostu się myli. Pozornie aplikacja może zachowywać się normalnie, a błąd może tkwić gdzieś indziej. Nie mówię tutaj bowiem o błędach wykrywanych w czasie kompilacji, które są najłatwiejsze do usunięcia, a sprowadza się to najczęściej do drobnych poprawek, często bardzo śmiesznych — typu dopisanie średnika na końcu wyrażenia. Najtrudniejsze do wykrycia są błędy zagnieżdżone w kodzie, kiedy program pozornie działa prawidłowo, lecz nie do końca wykonuje operacje zamierzone przez użytkownika. Warto, aby Czytelnik w tym momencie dobrze zapamiętał stwierdzenie, iż program zawsze działa prawidłowo, a gdy nie działa zgodnie z naszymi oczekiwaniami — jest to zwyczajnie wina projektanta.

Pomijam tutaj błędy kompilatora, bo one również są tworzone przez ludzi i również mogą zawierać błędy. Kompilator jest jednak produktem podwyższonego ryzyka: nie można pozwolić, aby zawierał choćby nawet najdrobniejsze błędy.

Błąd w aplikacji często jest określany mianem bug (z ang. robak, pluskwa). Termin ten wziął się z czasów, gdy komputery zajmowały duże pomieszczenia i pobierały tyle energii co małe osiedle, a obsługą owego „komputera” zajmował się sztab ludzi. Robaki zalęgnięte gdzieś w zakamarkach ogromnej maszyny czasami powodowały zwarcie instalacji elektrycznej. Od tamtej pory błędy nazywa się bugami, a proces ich wykrywania — debugowaniem (z ang. debugging).

W niniejszym rozdziale omówię kilka podstawowych, najczęstszych błędów popełnianych przez programistów. Zajmiemy się także debugerem Delphi, czyli programem służącym do wykrywania błędów w aplikacjach.

     1 Rodzaje błędów
     2 Opcje kompilatora
     3 Częste błędy programisty
          3.1 Niezainicjalizowane zmienne obiektowe
          3.2 Zwalnianie obiektów
          3.3 Tablice
     4 Wskaźniki
          4.4 Rejestry
          4.5 Stos
               4.5.1 LIFO
               4.5.2 FIFO
          4.6 Sterta
          4.7 Do czego służą wskaźniki?
          4.8 Tworzenie wskaźnika
          4.9 Przydział danych do wskaźników
               4.9.3 Uzyskiwanie adresu pamięci
          4.10 Tworzenie wskaźników na struktury
          4.11 Przydział i zwalnianie pamięci
          4.12 Wartość pusta
     5 Debuger Delphi
          5.13 Interfejs Debug
          5.14 Opcje projektu
               5.14.4 Gałąź Environment Block
          5.15 Punkty przerwań
               5.15.5 Ustawienie punktu przerwania
               5.15.6 Lista punktów przerwań
               5.15.7 Menu kontekstowe listy punktów przerwań
                    5.15.7.1 Dezaktywacja punktu przerwania
               5.15.8 Modyfikowanie punktów przerwań
               5.15.9 Zwykłe punkty przerwań
               5.15.10 Warunkowe punkty przerwań
          5.16 Polecenie Run to Cursor
          5.17 Podgląd zmiennych
               5.17.11 Menu kontekstowe listy wyrażeń testowych
               5.17.12 Okno właściwości
               5.17.13 Dodawanie elementów do listy
          5.18 Inspektor śledzenia
               5.18.14 Budowa okna
          5.19 Evaluate/Modify
               5.19.15 Pasek narzędzi
               5.19.16 Korzystanie z okna Evaluate/Modify
          5.20 Okno Call Stack
          5.21 Okno Local Variables
          5.22 Okno Thread Status
          5.23 Okno Event Log
               5.23.17 Menu kontekstowe kroniki śledzenia
               5.23.18 Kopiowanie zawartości do schowka
          5.24 Okno modułów
          5.25 Okno deasemblacji
          5.26 Polecenie Go to Address
          5.27 Okno Message View
     6 Praca krokowa
          6.28 Ikony na gutterze
          6.29 Przekraczanie i wkraczanie
     7 Opcje debugera
          7.30 Strona Borland Debuggers
          7.31 Zakładka Language Exceptions
          7.32 Zakładka Native OS Exceptions
          7.33 Zakładka Event Log
     8 Menu związane z debugerem
     9 Test
     10 FAQ
     11 Podsumowanie

W rozdziale:
*opowiem o kilku podstawowych błędach programistycznych,
*napiszę kilka słów o wskaźnikach w Delphi,
*przedstawię opcje kompilatora Delphi,
*omówię korzystanie z debugera Delphi.

Rodzaje błędów

Rozróżniamy kilka rodzajów błędów. Na przykład błędy powstałe na etapie projektowania są zwykłe niedopatrzeniami, często przypadkowymi pominięciami pewnych instrukcji lub są wynikiem użycia metody, która jest niedostępna w danej klasie. Kompilator Delphi działa jednak na tyle inteligentnie, że wskazuje linię, w której wystąpił błąd i — mało tego — wyświetla stosowny komentarz. Z takimi błędami jest w stanie sobie poradzić nawet mniej zaawansowany programista.

Innym rodzajem błędów są te powstające w trakcie działania programu, które są związane najczęściej z przekroczeniem zakresu tablicy, odwołaniem do nieistniejącego obiektu lub też zwyczajne błędy, związane z pamięcią. W efekcie zostaje wyświetlony zwykły komunikat informacyjny. Dlatego też dobrze jest używać wyjątków (try, except) w celu zapewnienia obsługi błędów i wyświetlania stosownego komunikatu. Często przyczyny błędów na etapie działania programu są banalne — np. próba załadowania pliku, który nie istnieje (owocuje to pojawieniem się wyjątku EFileNotFound). Dlatego też w takich sytuacjach należy sprawdzać przed załadowaniem pliku, czy on rzeczywiście istnieje. Taki błąd może okazać się na tyle banalny, że programista nawet nie weźmie go pod uwagę! Przykładowo, program do prawidłowego działania potrzebuje pliku XML, który domyślnie jest umieszczony w katalogu z programem. W tym momencie można zwyczajnie nie uwzględnić sytuacji, w której użytkownik przypadkowo lub celowo wykasuje ów plik XML. Wtedy program nie będzie mógł działać poprawnie. Błędy pojawiające się na etapie działania programu mogą okazać się trochę stresujące, lecz także nie są bardzo trudne do wykrycia.

Często można się spotkać z mylną opinią, iż program, który w trakcie kompilacji nie wykazał błędów, jest ich w ogóle pozbawiony! Oczywiste, że problemy zaczynają się dopiero później. Najtrudniejsze do wykrycia są błędy, których na ogół nie zauważa się na początku — wychodzą na jaw dopiero w trakcie dłuższego wykorzystywania aplikacji. Program z pozoru działa normalnie, lecz w pewnych, wydawałoby się losowych, sytuacjach działa niezgodnie z oczekiwaniami użytkownika. Sam niejednokrotnie spędziłem wiele godzin na poszukiwaniu usterki w kodzie. Ciągłe wpatrywanie się w kod oraz jego analiza może spowodować głowy, a gdy wreszcie kuriozalna przyczyna błędu wychodzi na jaw, nie wiadomo, czy należy odczuwać radość, czy też złość na siebie, że nie zauważyło się tak oczywistej usterki.

Opcje kompilatora

W opcjach projektu (menu Project => Options) znajduje się kategoria Compiler (rysunek 9.1), która także ma wpływ na sposób działania tworzonego programu.

9.1.jpg
Rysunek 9.1. Kategoria Compiler w oknie opcji projektu

Najbardziej interesujące dla programisty opcje mieszczą się w ramce Runtime errors (opis w tabeli 9.1). Co prawda, kompilator Delphi posiada wbudowany mechanizm sprawdzania, czy nie zachodzi próba odwołania się do elementu spoza tablicy, lecz opcje w ramce Runtime errors dodatkowo przydają się w trakcie działania programu.

Tabela 9.1. Ramka Runtime Error

Pozycja Przełącznik kompilacji Opis
Range checking {$R+} Włączenie tej opcji powoduje, iż Delphi sprawdza, czy nie zostały przekroczone zakresy tablic lub zmiennych String.
I/O checking {$I+} Sprawdzanie błędów wejścia-wyjścia.
Overflow checking {$Q+} Sprawdzanie, czy liczba Integer mieści się w odpowiednim zakresie.

W celu włączenia lub wyłączenia poszczególnych opcji z ramki Runtime erros można zastosować tzw. przełączniki kompilacji. Przykładowo, przełącznik {$R+} włącza opcję Range checking, a {$R-} — wyłącza ją.

Spójrz na poniższy kod:

procedure TForm1.Button1Click(Sender: TObject);
var
  S : String[10];
  B : Byte;
  A : array[0..1] of Integer;
begin
  S := 'To jest bardzo długi tekst, który się tu nie zmieści!';
  B := 256;
  A[2] := 1;
end;

Łatwo się zorientować, iż programista próbuje przypisać do zmiennej @@S@@ tekst, który jest dłuższy niż 10 znaków. Następnie zmiennej B (typu Byte) przypisano wartość 256. Zmienna typu Byte nie może przyjąć takiej wartości. W końcu do tablicy dwuelementowej (o indeksach 0 i 1) spróbowano przypisać dane o indeksie 2. O ile pierwsza instrukcja (ze zmienną @@S@@) skończy się jedynie wygenerowaniem ostrzeżenia kompilatora ([Warning] Unit1.pas(37): String constant truncated to fit STRING[10]), to dwa kolejne wiersze zostaną wskazane przez kompilator jako błędy ([Error] Unit1.pas(39): Constant expression violates subrange bounds). W przypadku gdy do zmiennej typu ciąg znakowy o ograniczonej długości zostanie przypisany zbyt długi tekst — zostanie on zwyczajnie obcięty.

W przypadku włączenia opcji Range checking każde przekroczenie zakresu spowoduje pojawienie się komunikatu Range error (błąd ten zresztą można obsłużyć samodzielnie). Jednakże w razie pozostawienia przełącznika kompilacji {$R-}, wewnętrzny mechanizm nie pozwoli na przypisanie do zmiennej niedozwolonej liczby w trakcie działania programu. Spójrzmy na poniższy fragment kodu:

procedure TForm1.Button1Click(Sender: TObject);
var
  B : Byte;
begin
{$R-}
  Randomize;
  try
    B := Random(99999);
    Caption := IntToStr(B);
  except
    raise Exception.Create('Za duża liczba!');
  end;
end;

Jeżeli powyższa procedura zostanie wklejona do aplikacji VCL.NET, łatwo będzie zauważyć, że każde naciśnięcie przycisku spowoduje wylosowanie liczby zawsze mniejszej od 256, tak aby wartość ta mogła się zmieścić w zakresie zmiennej typu Byte.

Modyfikując nieco kod i zmieniając przełącznik na {$R+}, prawie zawsze otrzymamy komunikat Za duża liczba!, gdyż prawdopodobieństwo wylosowania liczby mniejszej od 256 z zakresu od 0 do 99999 jest małe.

Częste błędy programisty

Skupmy się w tej chwili na typowych, bardzo powszechnych błędach programistycznych, które przeważnie skutkują wygenerowaniem komunikatów o błędach, nie załamując przy tym pracy programu.

Niezainicjalizowane zmienne obiektowe

Mam na myśli sytuację, w której projektant próbuje odwołać się do właściwości obiektu, bez uprzedniego wywołania jego konstruktora. W przypadku klas .NET większość z nich jest tak zbudowana, że może działać bez wywołania konstruktora za sprawą oznaczenia nazwy metody klauzulą class, np.:

type
  TMyClass = class
    class procedure DoIt;
  end;

Należy przy tym uważać, aby nie przekazywać do klasy żadnych zmiennych, bowiem brak instancji klasy powoduje, iż nie zaalokowano pamięci potrzebnej do przechowywania danych.

Jeżeli chodzi o VCL/VCL.NET, to w większości przypadków trzeba zapewnić wywołanie konstruktora. Jest to bezpieczniejsze, gdyż zmniejsza się prawdopodobieństwo, iż taki kod spowoduje jakieś błędy. Spójrzmy jednak na poniższą procedurę:

procedure TForm1.Button1Click(Sender: TObject);
var
  S : TStringList;
begin
  S.Add('Dane...');

end;

Zadeklarowałem w niej zmienną @@S@@, która wskazuje na klasę TStringList. Od razu, bez wywołania konstruktora, wywołałem jej metodę Add. Wykonanie takiego kodu skończy się wyświetleniem komunikatu o błędzie — takiego jak na rysunku 9.2.

9.2.jpg
Rysunek 9.2. Komunikat błędu

Rozwiązaniem tego problemu jest wywołanie konstruktora klasy:

procedure TForm1.Button1Click(Sender: TObject);
var
  S : TStringList;
begin
  S := TStringList.Create;
  S.Add('Dane...');
end;

Zwalnianie obiektów

W tradycyjnym modelu programowania Win32 zwalnianie obiektów metodą Free nie tylko należy do dobrych zwyczajów, ale jest wręcz koniecznością. Zainicjalizowana klasa rezerwuje pamięć, która dopóty nie zostanie zwolniona, zalega w pamięci. Jest to bardzo często popełniany błąd (podobnie zresztą postąpiłem w poprzednim przykładzie — nie użyłem metody Free).

Dobrym zwyczajem w trakcie tworzenia obiektów było kontrolowanie kodu za pomocą obsługi wyjątków:

procedure TForm1.Button1Click(Sender: TObject);
var
  S : TStringList;
begin
  S := TStringList.Create;
  try
    S.Add('Dane...');
    { kod programu }
  finally
    S.Free;
  end;
end;

Kod znajdujący się powyżej daje pewność, iż obiekt S zostanie zwolniony nawet w przypadku wystąpienia błędu (gwarantuje to użyte słowo kluczowe finally).

W .NET sytuacja nieco uległa zmianie, gdyż dzięki mechanizmowi garbage collection nie trzeba się już martwić o zwalnianie obiektów — następuje to automatycznie po zakończeniu działania programu. Nadal jednak dobrym zwyczajem pozostaje jawne zwolnienie obiektu: obiekt i pamięć są zwalniane w odpowiednim momencie, a zarazem dajemy do zrozumienia innym programistom, iż w tym miejscu kończy się działanie danej klasy.

Tablice

Opisywana sytuacja dotyczy zarówno tablic dynamicznych, jak i statycznych, a polega na próbie przypisania danych do nieistniejącego elementu. Co prawda kompilator jest na tyle odporny, iż wskaże próbę przypisania wartości do elementu spoza zakresu (jak pokazywałem przed chwilą), jednak nie dzieje się tak w przypadku tablic dynamicznych. Oto dwa przykłady:

procedure TForm1.Button1Click(Sender: TObject);
var
  S : array of String;
  A : array[1..2] of Byte;
  I : Byte;
begin
  SetLength(S, 1);
  S[2] := 'łańcuch';

  I := 4;
  A[I] := 12;
end;

Taki kod skompiluje się bezproblemowo — Delphi nie zauważy błędu na tym etapie. Za pomocą funkcji SetLength utworzono dwa elementy tablicy, jednak w dalszej części programu następuje próba przypisania danej pod indeks nr 2.

Natomiast w drugim przypadku mamy do czynienia z tablicą statyczną. Indeks jest określony przez zmienną @@I@@ — nie jest przypisany statycznie, zależy więc od wartości zmiennej @@I@@.

Oczywiście, żaden rozsądny programista nie zapisze kodu w taki sposób, jaki przedstawiłem powyżej, jednak takie same błędy mogą wystąpić w trakcie projektowania algorytmów (np. w funkcjach rekurencyjnych — wywołujących same siebie — lub w pętlach).

Błędy wyjścia poza indeks mogą się również przydarzyć podczas programowania pętli:

procedure TForm1.Button1Click(Sender: TObject);
var
  A : array[1..10] of Byte;
  I : Byte;
begin

  for I := 0 to 10 do { kod }

end;

W takim przypadku można nie zauważyć, że najmniejszym elementem tablicy @@A@@ jest 1, a największym — 10. Często (na przykład ja tak robię) odruchowo programujemy pętle for, przypisując licznikowi (zmiennej @@I@@) wartość początkową 0. Rozwiązaniem tego problemu jest stosowanie funkcji Low i High.

Wskaźniki

Wielu początkującym programistom najwięcej problemu przysparzają wskaźniki. Jest to dość specyficzny element języka programowania, który jest obecny w większości środowisk — od Delphi przez C na PHP kończąc. Niestety na platformie .NET wskaźniki są tzw. elementem niebezpiecznym (ang. unsafe) i korzystanie z nich nie jest możliwe. Istnieją, co prawda, wyjątki od tej reguły i są znane pewne sposoby na ominięcie tego problemu (patrz rozdział 11. „Migracja do .NET”), ale Microsoft ani Borland nie zalecają ich używania. Dla niektórych nie jest to zbyt pocieszające — wszak wskaźniki były istotnym elementem programowania, wielu więc straciło tym samym potężne narzędzie do manipulowana danymi i pamięcią.

Jednak wskaźniki mogą być w dalszym ciągu wykorzystywane na platformie Win32, oczywiście przy użyciu Delphi. Pragnę omówić teraz ten niewątpliwie istotny element języka programowania.

Warto więc dodać, że wskaźniki były jedną z najczęstszych (o ile nie najczęstszą) przyczyną występowania problemów w aplikacjach. Wymagały bowiem operowania na pamięci, co często kończyło się błędami typu AccessViolation.

Mimo że nie będziemy już pewnie w Delphi dla .NET korzystali ze wskaźników, chciałbym wspomnieć o paru pojęciach, które być może będą przydatne podczas lektury dalszej części rozdziału. Chciałbym też, aby każdy znał podstawowe pojęcia, takie jak sterta czy stos, bo w końcu w tym rozdziale mówię o niskopoziomowych funkcjach, na jakich operuje debuger.

Rejestry

Rejestr jest specjalnym obszarem pamięci wbudowanym w CPU (Central Processing Unit). Języki wysokiego poziomu zapewniają wygodne funkcje, jak na przykład Writeln, która realizują pewne działanie — wyświetlanie tekstu na konsoli. W rzeczywistości jednak jedna instrukcja Writeln odpowiada kilku instrukcjom procesora.

Istnieje także coś takiego jak wskaźnik instrukcji, kontrolujący, która instrukcja zostanie wykonana jako następna. W ten sposób działa program, który jest wykonywany przez procesor. Mówię o tym dlatego, aby zapoznać Czytelnika z paroma pojęciami.

Stos

Stos jest tym obszarem pamięci komputera, która jest alokowana (przydzielana) w momencie uruchamiania jakiegoś programu. System operacyjny w tym momencie musi określić, ile pamięci będzie potrzebował do prawidłowego działania programu. Z pojęciem stosu wiąże się również akronim LIFO.

LIFO

LIFO oznacza Last In, First Out (ostatni wchodzi, pierwszy wychodzi). LIFO jest rodzajem kolejki — algorytmem operacji na stosie: stanowi uporządkowaną strukturę liniową. Podczas wykonywania jakiejś funkcji programu, na stos są odkładane pewne dane (na sam wierzch), po czym — po wykonaniu operacji — są od razu zdejmowane.

Aby ułatwić zrozumienie tego pojęcia, często podaje się przykład z monetami lub z talerzami: układając monety jedną na drugiej, tworzymy z nich wieżę. Teraz, aby zdjąć wszystkie elementy tej wieży, trzeba zacząć od monety ułożonej na samej górze. Identycznie działa stos — nie można zdjąć żadnych danych ułożonych w środku, lecz trzeba zacząć od tych na samym wierzchu.

FIFO

Innym mechanizmem kolejkowania jest FIFO — First In, Fist Out (pierwszy wchodzi, pierwszy wychodzi). Wspominam o tym jako o ciekawostce, z którą Czytelnik także może się spotkać — algorytm ten nie ma nic wspólnego ze stosem.

FIFO można porównać do kolejki sklepowej — pierwszy klient w kolejce kupuje pierwszy, po czym pierwszy wychodzi. To samo można powiedzieć o drukarce — najpierw drukuje ona pierwsze strony, potem kolejne, aż w końcu drukuje ostatnią i kończy działanie.

Sterta

Powiedziałem, że stos jest pamięcią alokowaną w momencie działania programu. Na przykład parametry funkcji oraz zmienne lokalne są układane na stosie w momencie wywołania funkcji, natomiast po jej zakończeniu jej działania są ze stosu zdejmowane.

Sterta stanowi całą dostępną pamięć, którą można wykorzystać w trakcie działania programu. Do tego właśnie m.in. służą wskaźniki. Zawsze gdy zajdzie taka potrzeba, można w trakcie działania programu zarezerwować sobie pewien blok pamięci — np. 1 MB. W tym momencie ten 1 megabajt może być wykorzystywany w dowolny sposób, w dowolnym miejscu w programie.

Jest tylko jedna niedogodność — gdy dane przestaną już być potrzebne, programista samodzielnie musi zadbać o zwolnienie pamięci.

Do czego służą wskaźniki?

Ogólnie mówiąc, służą do manipulowania danymi. Dzięki wskaźnikom mogliśmy zaalokować dowolny obszar pamięci na stercie. Wskaźniki umożliwiały również pobranie adresu miejsca w pamięci, w jakim znajduje się dana zmienna, a także manipulację tymi danymi. Programiści języka C lub innego języka, w którym wskaźniki są nieodłączonym elementem, znają prezentowane zagadnienia i w tym momencie zapewne są zadowoleni z faktu, że wskaźniki w Delphi dla .NET nie istnieją (jeżeli nie wiedzieli o tym wcześniej).

Tworzenie wskaźnika

Na samym początku muszę zaznaczyć, że prezentowane przeze mnie przykładowe fragmenty kodu działają poprawnie jedynie w środowisku Win32 (na platformie .NET istnieją pewne ograniczenia co do stosowania wskaźników, ale o tym opowiem w rozdziale 11.).

Czytelnik pewnie pamięta, że w rozdziale 3. wspominałem o metodach przekazywania parametrów do funkcji. Jedynym ze sposobów jest przekazywanie przez referencje, co polega na umieszczeniu przed nazwą parametru słowa kluczowego var. W takim przypadku zawartość zmiennej nie jest kopiowana do funkcji, tak jak to ma miejsce w przypadku przekazywania parametru przez wartość. Przekazywany jest jedynie adres komórki w pamięci, w której są umieszczone dane. Spójrzmy na poniższy przykład:

program Pointers;

{$APPTYPE CONSOLE}

procedure Foo(var Value : String);
begin
  Value := Value + ' jest fajne';
end;

var
  S : String;
begin
  S := 'Delphi';
  Foo(S);

  Writeln(S);
  Readln;
end.

Na samym początku zadeklarowałem zmienną @@S@@, do której przypisałem wartość Delphi. Następnie zmienną @@S@@ przekazano jako parametr do procedury Foo. W procedurze Foo zmodyfikowałem parametr @@Value@@, nadając mu nową wartość. W rzeczywistości zarówno @@S@@, jak i @@Value@@ wskazują na tę samą komórkę pamięci, tak więc przez modyfikację wartości @@Value@@ zmieniłem również wartość zmiennej @@S@@.

Przekazywanie parametrów przez referencję jest szybsze i zabiera mniej pamięci.
Wskaźniki są zmiennymi, które wskazują na inną zmienną.

Zapewne powyższa wskazówka niewiele wyjaśnia. Wskaźniki są specjalnym typem danych — w pamięci nie są przechowywane dane (zawartość zmiennej), lecz jedynie odpowiadające im adresy komórki pamięci.

Zadeklarowanie wskaźnika następuje za pomocą operatora (^).

var
  P : ^String;

Od tego momentu w programie można korzystać ze wskaźnika @@P@@, wskazującego na typ String. We wszelkich operacjach dokonywanych na wskaźnikach muszą być wykorzystywane dwa operatory specyficzne jedynie dla typów wskaźnikowych — są to operatory ^ oraz @. Ich znaczenie opiszę w dalszej części tego rozdziału.

Przydział danych do wskaźników

Na samym początku przeprowadźmy pewien test. Spróbujmy uruchomić taki program:

program Pointers;

var
  P : ^String;

begin
  P^ := 'Delphi 2005';
end.

Program próbuje przypisać określone dane do wskaźnika w postaci ciągu tekstowego. Konieczne tu jest wykorzystanie operatora ^, w przeciwnym wypadku Delphi zasygnalizuje błąd: [Error] Pointers.dpr(12): Incompatible types: 'String' and 'Pointer'.

Próba uruchomienia takiego programu zakończy się jednak błędem typu Runtime (patrz rysunek 9.3).

9.3.jpg
Rysunek 9.3. Komunikat o błędzie wyświetlony po uruchomieniu programu

Przyczyną zaistnienia błędu jest fakt, że do typu wskaźnikowego nie można przypisać wartości w normalny sposób. Wskaźniki muszą uprzednio wskazywać na inną, zwykłą zmienną.

O przydzielaniu danych bezpośrednio do wskaźnika opowiem się w dalszej części tego rozdziału.

Poniższy program zostanie skompilowany i, co najważniejsze, będzie działał bez problemu:

program Pointers;

{$APPTYPE CONSOLE}

var
  S : String;
  P : ^String;

begin
  S := 'Delphi'; // przypisanie danych do zwykłej zmiennej
  P := @S; // uzyskanie adresu zmiennej
  P^ := 'Delphi jest fajne'; // modyfikacja danych

  Writeln(S);
  Readln;
end.

Po uruchomieniu takiego programu na konsoli zostanie wyświetlony napis Delphi jest fajne, mimo iż odwołano się do zmiennej @@S@@, która uprzednio miała wartość Delphi.

Uzyskiwanie adresu pamięci

We wcześniejszych fragmentach rozdziału pokazałem zastosowanie operatora @ do uzyskania adresu komórki pamięci, w której znajduje się wartość zmiennej @@S@@. Następnie przypisując dane do zmiennej wskaźnikowej @@P@@, zmieniłem jednocześnie wartość zmiennej @@S@@.

Tworzenie wskaźników na struktury

Można się zastanawiać, do czego służą wskaźniki? Założenie jest takie, że podczas tworzenia jakichś struktur — zarówno tablic, jak i rekordów — nie jest konieczne manipulowanie wielkimi blokami pamięci. Wystarczy tylko utworzyć wskaźnik tego rekordu i ewentualnie modyfikować w ten sposób dane, zamiast tworzyć kolejną instancję (kopię) rekordu.

Podczas tworzenia jakiegoś rekordu wskazane jest utworzenie nowego typu wskaźnikowego, wskazującego na ten rekord. Po wypełnieniu danych następuje przekazanie do procedury jedynie wskaźnika tego rekordu:

program PRecApp;

uses
  Dialogs;

type
  TInfoRec = packed record
    FName : String[30];
    SName : String[30];
    Age : Byte;
    Pesel : Int64;
    Nip : String[60]
  end;
  PInfoRec = ^TInfoRec; // utworzenie wskaźnika

  procedure SomeProc(InfoRec : PInfoRec);
  begin
    ShowMessage('Dotychczasowa wartość InfoRec.FName to ' + InfoRec.FName + '. Zmieniam na Adam');
    InfoRec.FName := 'Adam'; // zmiana danych
  end;

var
  InfoRec: TInfoRec;

begin
  InfoRec.FName := 'Jan';
  InfoRec.SName := 'Kowalski';
  InfoRec.Age := 41;
  InfoRec.Pesel := 55012010013;
  InfoRec.Nip := '34234–23432–23423';

  SomeProc(@InfoRec);
  ShowMessage(InfoRec.FName); // wyświetlenie zmienionej wartości

end.

Spójrzmy na powyższy przykład. Na samym początku zadeklarowałem rekord, do którego przydzieliłem dane — informacje na temat osoby Jana Kowalskiego. Następnie do procedury SomeProc przekazałem adres komórki pamięci, w której jest umiejscowiony rekord. Procedura SomeProc, znajdując adres komórki pamięci, modyfikuje dane rekordu.

Przydział i zwalnianie pamięci

Na samym początku omawiania wskaźników zaprezentowałem przykład, w którym próbowałem przydzielić dane do wskaźnika. Uruchomienie tamtego programu skończyło się błędem z powodu próby przypisania danych do zmiennej wskaźnikowej, mimo iż nie pobrano adresu komórki (za pomocą operatora @). Istnieje możliwość dynamicznej alokacji pamięci w trakcie działania programu. Służy do tego funkcja New oraz Dispose. Oto przykład:

program NewPointer;

uses
  Dialogs;

type
  TInfoRec = packed record
    FName : String[30];
    SName : String[30];
    Age : Byte;
    Pesel : Int64;
    Nip : String[60]
  end;
  PInfoRec = ^TInfoRec; // utworzenie wskaźnika


var
  InfoRec: PInfoRec;

begin
  New(InfoRec);

  InfoRec^.FName := 'Jan';
  InfoRec^.SName := 'Kowalski';
  InfoRec^.Age := 41;
  InfoRec^.Pesel := 55012010013;
  InfoRec^.Nip := '34234–23432–23423';

  ShowMessage(InfoRec^.FName); // wyświetlenie zmienionej wartości

  Dispose(InfoRec);
end.

Jak widać, w tym przykładzie nie pobrano adresu komórki żadnego rekordu. Zwyczajnie, na samym początku została zaalokowana (funkcja New) pamięć na potrzeby naszej zmiennej wskaźnikowej (@@InfoRec@@), aby później przypisać dane.

W celu zaalokowania pamięci można posłużyć się także funkcjami GetMem i FreeMem. Funkcja GetMem wymaga wpisania dodatkowego parametru, jakim jest liczba bajtów przeznaczonych do alokacji. Dane te uzyskuje się, wywołując funkcję SizeOf — np.:

GetMem(InfoRec, SizeOf(InfoRec));

Zalecane jest jednak użycie funkcji New i Dispose.</dfn>

Wartość pusta

Nieraz podczas programowania spotkamy się z instrukcją nil. Instrukcja ta jest używana wraz ze wskaźnikami i oznacza wartość pustą.

Wskaźnik := nil;

Taki zapis spowoduje, że do wskaźnika nie będą aktualnie przypisane żadne wartości.

Debuger Delphi

Debuger Delphi jest podstawowym mechanizmem zapewniającym śledzenie wykonania programu i wykrywanie błędów. Debuger dostarcza wielu informacji na temat wykonywanego programu, ułatwiając tym samym znalezienie niepoprawnego kodu. Generalnie, uruchomienie debugera następuje po wybraniu z menu Run opcji Run lub użyciu skrótu klawiszowego — F9. Debuger jest zintegrowany z Delphi i działa cały czas — od projektanta zależy, jak zostanie wykorzystany. Parę najbliższych stron zostanie poświęconych funkcjonalności debugera w Delphi.

Istnieje możliwość uruchamiania programów bez ingerencji debugera. W takim przypadku z menu Run wybiera się opcję Run Without Debugging.

Interfejs Debug

Z menu View => Desktops należy wybrać Debug Layout (rysunek 9.4). Równie dobrze można też skorzystać z paska narzędziowego Desktop. W tym momencie powinien zostać wyświetlony tryb pracy z oknami debugera (taki tryb pracy powinien być przez Delphi uruchamiany automatycznie w momencie wywołania opcji Run).

9.4.jpg
Rysunek 9.4. Interfejs Delphi z trybem pracy Debug

Miejsce inspektora obiektów i projektanta formularzy zastępują okna, dające podgląd na zmienne, wątki oraz inne elementy aplikacji.

Opcje projektu

Okno opcji projektu (Project => Project Options) zawiera jeszcze jedną kategorię, o której nie mówiłem. Jest to kategoria Debugger (rysunek 9.5), która określa kilka parametrów startowych programu. Tabela 9.2 opisuje opcje dostępne w tej kategorii.

Okno opcji Debugger można wywołać także z menu Run => Parametrs.

9.5.jpg
Rysunek 9.5. Kategoria Debugger

Tabela 9.2. Opcje kategorii Debugger

Opcja Opis
Host Application Można wybrać aplikację, która będzie uruchamiana przez debuger. Jeżeli debuger Delphi ma analizować jedynie tworzony projekt — pole to należy pozostawić puste.
Parameters Parametry, które będą przekazywane aplikacji w momencie jej uruchamiania.
Working Directory Nazwa katalogu, który będzie wykorzystywany przez debuger. Domyślnie jest to katalog z projektem Delphi.

Jeżeli chodzi o parametry przekazywane do aplikacji, to mechanizm ten działa na zasadzie aplikacji DOS, aplikacji konsolowych czy uniksowych. W takim przypadku parametry przekazywane do programu stanowią podstawową informacje o tym, jak program ma się zachowywać. Przykładowo, wywołując kompilator C#, należy napisać:

csc C:\csharp.cs

W takim przypadku parametrem jest fraza C:\csharp.cs.

W kategorii Debugger także można określić frazę, która będzie przekazywana do programu. Istnieje możliwość odczytania takiego parametru za pomocą starej funkcji ParamStr lub klasy .NET — Environment. Oto przykład wyświetlania parametru określonego w oknie Debugger:

procedure TForm1.Button1Click(Sender: TObject);
begin
  MessageBox(Handle, ParamStr(1), 'Parametr', MB_OK);
  MessageBox(Handle, Environment.get_CommandLine, 'Parametr programu', MB_OK);
end;

Powyższy przykład korzysta z funkcji Win32API — MessageBox.

W pierwszym przypadku, korzystając z funkcji ParamStr, należy w nawiasie podać numer odczytywanego parametru (możliwe jest przekazanie numerów kilku parametrów). Jeżeli chodzi o klasę Environment, funkcja (metoda) get_CommandLine podaje zarówno parametry przekazane do funkcji, jak i ścieżkę do danego programu. Osobiście preferuję metodę pierwszą — nie tylko ze względu na przyzwyczajenie, bowiem istnieje możliwość określenia numeru parametru i funkcja zwraca tylko parametr, a nie dodatkowy tekst określający ścieżkę naszego programu, jak to ma miejsce w przypadku klasy Environment (tutaj sytuacja jest bardziej skomplikowana).

Gałąź Environment Block

Zmienne środowiskowe są danymi zawierającymi informacje o systemie, np. ścieżkę katalogu Windows, nazwę zalogowanego użytkownika itp. Zakładka Environment Block zawiera listę zmiennych środowiskowych. Do tego istnieje możliwość dodania własnych zmiennych. Dane ustawione w ten sposób można odczytać, korzystając z klasy Environment, a konkretnie z funkcji GetEnvironmentVariable:

 MessageBox(Handle, Environment.GetEnvironmentVariable('windir'), 'Katalog Windows', MB_OK);

Powyższa instrukcja spowoduje wyświetlenie w oknie ścieżki katalogu Windows.

Punkty przerwań

Punkty przerwań (ang. breakpoints) umożliwiają wstrzymanie wykonywania programu w określonym momencie. Najczęściej używanym typem punktu przerwań jest źródłowy punkt przerwania (ang. source breakpoint) — w momencie wykonywania programu, gdy aplikacja napotyka na przerwanie, wstrzymuje działanie programu. Dalsze działanie może odbywać się jako tzw. praca krokowa lub ciągła.

Ustawienie punktu przerwania

Jednym ze sposobów ustawiania punktu przerwania jest użycie klawisza skrótu F5. Linia, w której znajduje się kursor, zostanie wyróżniona kolorem fioletowym (zakładając domyślnie ustawienia Delphi), a instrukcja znajdująca się w tej linii nie zostanie wykonana, dopóki użytkownik na to nie zezwoli. Łatwo się o tym przekonać. Bądź to w aplikacji VCL.NET lub WinForms, bądź w aplikacji konsolowej należy zaznaczyć konkretną linię kodu, a następnie nacisnąć klawisz F5. Najlepiej będzie zaznaczyć kilka punktów przerwań. Teraz można uruchomić program i obserwować jego działanie. Przed każdym punktem przerwania nastąpi wstrzymanie wykonywania programu, a wykonywana linia kodu będzie zaznaczana na tzw. gutterze (szara belka znajdująca się po lewej stronie edytora kodu) — patrz rysunek 9.6.

9.6.jpg
Rysunek 9.6. Kod źródłowy z zaznaczonymi punktami przerwań

W celu kontynuowania wykonywania programu należy nacisnąć przycisk F9.

Punkt wykonania wskazuje na linię, która zostanie wykonana w następnej kolejności. W Delphi 2005 punkt wykonania jest symbolizowany ikoną zielonej strzałki.

Innym sposobem zaznaczenia punktu przerwania jest kliknięcie guttera lewym przyciskiem myszy. W efekcie zostanie zaznaczona linia odpowiadająca pozycji kursora w momencie kliknięcia.

Ustawianie punktów przerwań jest często dobrą metodą wykrywania momentu, w którym program przestaje funkcjonować poprawnie. Wystarczy zaznaczyć kilka linii i uruchomić program. Dzięki wykonywaniu poszczególnych instrukcji krokowo (program zatrzymuje się na kolejnym punkcie przerwania), uzyskujemy możliwość zaobserwowania, w którym momencie występuje błąd.

Lista punktów przerwań

Okno, w którym są wyświetlane informacje na temat wszystkich punktów przerwań, jest nazywane Breakpoint List (ang. lista przerwań) i można je wywoływać z menu View => Debug Window => Breakpoint List (rysunek 9.7).

9.7.jpg
Rysunek 9.7. Okno listy przerwań

Okno listy punktów przerwań jest podzielone na kilka kolumn:

*FileName/Address — nazwa pliku źródłowego, w którym został ustawiony punkt przerwania;
*Line/Length — wskazuje linię, w której został ustawiony punkt przerwania;
*Condition — wyświetla wyrażenie warunkowe, jakie zostało zdefiniowane dla warunkowego punktu przerwania;
*Action — operacja, która będzie wykonywana w momencie napotkania punktu przerwania; domyślnie — wstrzymanie wykonywania;
*Pass Count — warunkowy licznik przejść ustawiony dla punktu przerwania. Będę o tym mówił w dalszej części rozdziału.

Menu kontekstowe listy punktów przerwań

Okno listy punktów przerwań posiada dwa menu. Jedno jest widoczne podczas kliknięcia konkretnej pozycji na liście, a drugie — w przypadku, gdy żadna pozycja na liście nie została zaznaczona. Tabela 9.3 zawiera listę pozycji z menu wraz z opisami.

Tabela 9.3. Menu kontekstowe okna listy punktów przerwań

Element Opis
Enabled Pozycja domyślnie jest zaznaczona i określa, czy punkt przerwania jest aktywny.
Delete Usuwa zaznaczony punkt; skrót klawiaturowy — klawisz Delete.
View Source Wybranie tej pozycji powoduje wyświetlenie okna edytora kodu.
Edit Source Wybranie tej pozycji powoduje wyświetlenie okna edytora kodu oraz przejście do linii, w której jest ustawiony punkt przerwania.
Properties Wyświetla okno dodatkowych właściwości związanych z punktem przerwania.
Breakpoints Wybranie tej pozycji powoduje rozwinięcie dodatkowego menu, w którym istnieje możliwość dodania nowego punktu, skasowania wszystkich punktów lub ich dezaktywację.
Stay On Top Zaznaczenie tej opcji spowoduje, że okno będzie zawsze na wierzchu.
Dockable Zaznaczenie tej opcji zapewnia możliwość dokowania okna listy punktów przerwań.
Dezaktywacja punktu przerwania

Dezaktywacja punktu przerwania jest możliwa po wybraniu opcji Enabled dostępnej w menu kontekstowym. Na pewno taki sposób pracy jest wygodniejszy niż całkowite usunięcie punktu. Wszystkie dane nadal pozostają na liście, lecz debuger nie będzie przerywał działania programu, napotykając na dezaktywowany punkt przerwań.

Modyfikowanie punktów przerwań

Drugie menu kontekstowe związane z oknem listy punktów przerwań umożliwia dodanie nowego punktu, usunięcie wszystkich pozycji lub ich dezaktywację.
Uzyskanie dostępu do okna właściwości punktu przerwania (rysunek 9.8) umożliwia skrót klawiaturowy Ctrl+A lub pozycja z menu kontekstowego — Properties (albo pozycja z drugiego menu kontekstowego — Add/Source Breakpoint).

9.8.jpg
Rysunek 9.8. Okno właściwości punktu przerwania

Okno w wersji standardowej posiada kontrolki umożliwiające zmianę numeru wiersza (Line number), w którym znajduje się punkt przerwania, bądź ustawienie grupy, do której punkt należy. Pozycje Condition oraz Pass count służą do zaawansowanego ustawiania punktów przerwań, ale o tym opowiem za chwilę.

Przycisk Advanced rozwija okno, umożliwiając określenie zaawansowanych opcji punktu przerwań. Jest to związane z reakcją na wykrycie przez debuger punktu przerwania. Opis dodatkowych opcji znajduje się w tabeli 9.4.

Tabela 9.4. Zaawansowane opcje okna właściwości punktu przerwania

Element Opis
Break Domyślna reakcja (wstrzymanie wykonywania aplikacji), wykonywana w momencie napotkania na punkt przerwań.
Ignore subsequent exceptions Zaznaczenie spowoduje zignorowanie ewentualnych błędów powstałych w ramach wykonywania dotychczasowej sesji debugowania.
Handle subsequent exceptions Opcja ta wyklucza Ignore subsequent exceptions. Powoduje bowiem wstrzymanie działania programu w momencie napotkania błędu.
Log message Tekst wprowadzony do tej kontrolki zostanie dodany do okna Event Log (o tym opowiem nieco dalej) okna zdarzeń.
Eval expression Kontrolka może zawierać fragment kodu, który zostanie wykonany w momencie wykrycia punktu przerwania.
Log result Zaznaczenie opcji spowoduje, iż kod z kontrolki Eval expression zostanie dodany do okna Event Log.
Enable group, Disable group Uaktywnia lub dezaktywuje punkty przerwań z konkretnych grup.

W Delphi 4 w oknie właściwości punktu przerwań dodano nowe pole — Keep existing Breakoint. Jego zaznaczenie spowoduje, iż zmiany dokonane w oknie właściwości w rzeczywistości spowodują utworzenie nowego punktu przerwań, o nowych właściwościach — pierwowzór pozostaje więc w swojej nienaruszonej postaci.

Wykonajmy pewne proste ćwiczenie, związane z polem Eval expression. Mając prosty kod źródłowy (np. taki jaki jest pokazany na listingu 9.1), należy ustawić punkt przerwań na linii Console.ReadLine;. We właściwościach punktu przerwań w kontrolce Eval expression wpiszemy Console.WriteLine('Punkt przerwań...');.

Listing 9.1. Przykładowy program

program Project3;

{$APPTYPE CONSOLE}

var
  I : Integer;
begin
  for I := 0 to 120 do
    Console.WriteLine('Iteracja nr {0}, wartość {1}',
                      Convert.ToString(i), Convert.ToString(i * 2));


  Console.ReadLine;


end.

Wykonując takie ćwiczenie i uruchamiając program, można zaobserwować ciekawe rezultaty. Program oprócz tego, że wstrzymał działanie przed punktem przerwania, wykonał dodatkowo działanie określone kontrolką Eval expression.

Zwykłe punkty przerwań

Dotychczas mówiłem o tak zwanych zwykłych punktach przerwań. Nie wymagają one bardziej szczegółowych wyjaśnień, gdyż domyślnie powodują jedynie wstrzymanie działania aplikacji. W większości przypadków wystarczy stosowanie jedynie zwykłych punktów przerwań, aczkolwiek istnieje możliwość zastosowania także nieco bardziej skomplikowanych operacji — warunkowych punktów przerwań.

Warunkowe punkty przerwań

Warunkowe punkty przerwań dzielą się na dwie kategorie. Pierwsza z nich to tzw. punkty przerwań z wyrażeniem warunkowym. Wyrażenie warunkowe należy wpisać w kontrolce Condition okna właściwości punktu przerwań. Spójrzmy na listing 9.1. Załóżmy, że trzeba ustawić punkt przerwań w linii:

Console.WriteLine('Iteracja nr {0}, wartość {1}',
Convert.ToString(i), Convert.ToString(i * 2));

Jako że owa linia znajduje się wewnątrz pętli for, praca programu byłaby wstrzymywana przy każdej iteracji, co stanowiłoby niepotrzebną stratą czasu (szczególnie przy większej liczbie iteracji) oraz niepotrzebnym wysiłkiem programisty.

Funkcja WriteLine posiada pewną właściwość, o której nie wspominałem wcześniej. Działa ona jak funkcja Format — ma możliwość zagnieżdżania danych w ciągu znakowym bez konieczności rozdzielania i łączenia tekstu operatorem +. W takim przypadku w ciągu znaków należy umieścić znaczniki: {0} oraz {1}, które podczas działania programu zostaną zastąpione poprzez dane przekazane do funkcji WriteLine, tak jak na listingu 9.1.

Teraz można wpisać w polu Condition warunek I>115. Dzięki temu przed każdym wstrzymaniem programu debuger będzie sprawdzał, czy zmienna @@I@@ jest większa niż 115. Jeżeli tak będzie, praca program zostanie wstrzymana, natomiast w przeciwnym przypadku — będzie kontynuowana.

Innym rodzajem warunkowych punktów przerwań są punkty przerwań z warunkiem liczby przejść. W tym przypadku działanie programu zostanie wstrzymane tylko wtedy, gdy punkt przerwania zostanie osiągnięty odpowiednią liczbę razy. Przykład zastosowania zaprezentuję na programie z listingu 9.1. W oknie właściwości punktu przerwań, w kontrolce Pass count wpiszmy 100. Po uruchomieniu programu pętla będzie wykonywana dopóty, dopóki wartość zmiennej @@I@@ nie osiągnie 100 — wówczas praca programu zostanie wstrzymana.

Liczba przejść jest określana względem jedynki, a nie zera, tak więc liczba przejść równa 100 oznacza, że punkt przerwania stał się aktywny przy setnym napotkaniu.

Warunkowe punkty przerwań spowalniają normalny proces, gdyż za każdym napotkaniem tego typu punktu musi nastąpić sprawdzanie warunku. Jeżeli więc dany program będzie działał zbyt wolno — dobrze jest sprawdzić swoją listę przerwań.

Polecenie Run to Cursor

Poleceniem Run to Cursor można zastąpić punkt przerwań — można je traktować jako tymczasowy, jednorazowy sposób na wstrzymanie pracy programu w miejscu określonym przez kursor w edytorze kodu.

Polecenie Run to Cursor wywołuje się zarówno z menu Run => Run to Cursor, jak i z menu kontekstowego edytora kodu (Debug => Run to Cursor). Skrótem klawiszowym jest F4.

Podgląd zmiennych

Podczas wstrzymania działania aplikacji, czy to wywołanej poleceniem Run to Cursor, czy też punktami przerwań, istnieje możliwość podglądu wartości wybranej zmiennej (zmiennych). Okno, w którym możemy zaobserwować wartości zmiennych, jest pokazane na rysunku 9.9.

9.9.jpg
Rysunek 9.9. Okno Watch List

Okno Watch List ma prostą konstrukcję — lewa kolumna zawiera nazwę zmiennej, a prawa — wartość owej zmiennej.

Menu kontekstowe listy wyrażeń testowych

Okno Watch List jest nazywane oknem wyrażeń testowych. Jak każde okno w Delphi, również i ono posiada menu kontekstowe. Opis elementów menu znajduje się w tabeli 9.5.

Tabela 9.5. Opis listy elementów menu

Element Opis
Edit Watch Umożliwia edycję zaznaczonej pozycji.
Add Watch Dodaje nowy element do listy wyrażeń testowych.
Enable Watch Uaktywnia element z listy.
Disable Watch Dezaktywuje element z listy.
Delete Watch Usuwa zaznaczony element.
Copy Watch Value Kopiuje aktualną wartość zmiennej do schowka.
Copy Watch Name Kopiuje aktualną nazwę zmiennej do schowka.
Enable All Watches Uaktywnia wszystkie elementy z listy.
Disable All Watches Dezaktywuje wszystkie elementy z listy.
Delete All Watches Usuwa wszystkie elementy z listy.
Add Group Umożliwia dodanie grupy wyrażeń testowych (tworzy nową zakładkę w oknie).
Delete Group Usuwa wybraną grupę (na którą wskazuje zakładka w oknie).
Move Watch to Group Przesuwa aktywną pozycję do innej wybranej grupy.
Show Column Headers Ukrywa lub pokazuje nagłówki kolumn w oknie.
Stay On Top Określa, czy okno ma być zawsze na wierzchu.
Dockable Włącza lub wyłącza opcje dokowania okna.

Prawdopodobnie większość z opcji w menu kontekstowym nie będzie przez Czytelnika wcale wykorzystywana. Najczęściej używanymi opcjami tego menu są Add Watch oraz Edit Watch.

Okno właściwości

Wybranie polecenia Edit Watch (Ctrl+E) lub Add Watch (Ctrl+A) spowoduje wyświetlenie okna Watch Properties (rysunek 9.10).

9.10.jpg
Rysunek 9.10. Okno właściwości

Główną kontrolką w tym oknie jest Expression, gdzie należy wprowadzić nazwę śledzonej zmiennej. Z listy rozwijalnej możemy również wybrać wcześniej wprowadzone pozycje.

Druga lista rozwijalna, Group name, służy do określania grupy dla wybranej pozycji. Domyślną grupą jest Watches.

Kontrolka Repeat count ma zastosowanie podczas podglądu zmiennej będącej tablicą. Jeżeli obserwujemy tablicę, to w oknie Watch List zostaną wyświetlone wartości jej wszystkich elementów. Aby ograniczyć tę liczbę do, powiedzmy, 10 elementów, w kontrolce Repeat count wpisz liczbę 10.

Wartość z pola Digits przydaje się jedynie w przypadku liczb zmiennoprzecinkowych, bowiem wartość wprowadzona w tej kontrolce oznacza liczbę miejsc po przecinku, jakie będą wyświetlane w oknie Watch List.

Pozostałe opcje w ramce dają możliwość przedstawiania wartości zmiennych w różnej formie (np. liczbowej, znakowej czy heksadecymalnej). Należy wybrać odpowiednią pozycję. Domyślnie jest to Record/Structure.

Najszybszym sposobem na uzyskanie dostępu do właściwości elementu w oknie Watch List jest podwójne kliknięcie myszą jego nazwy.

Dodawanie elementów do listy

Najszybszą metodą dodawania zmiennej do listy obserwowanych zmiennych jest umieszczenie wskaźnika myszy nad jej nazwą, a następnie wybranie z menu kontekstowego pozycji Debug => Add Watch to Cursor (Ctrl+F5).

Inspektor śledzenia

Inspektor śledzenia jest opcją dodaną w Delphi 4. Mówiąc krótko, umożliwia podgląd całych struktur danych, takich jak klasy czy rekordy. Umożliwia wyświetlanie szczegółowych informacji na temat wybranego obiektu (rysunek 9.11).

9.11.jpg
Rysunek 9.11. Inspektor śledzenia

Informacje są wyświetlane w oknie bliźniaczo podobnym do inspektora obiektów, także podzielonym na zakładki. Inspektor śledzenia można włączyć jedynie w przypadku, gdy program jest uruchomiony i działa pod kontrolą debugera.

Teraz utwórzmy nowy projekt WinForms i punkt przerwań na linii:

  InitializeComponent;

Po uruchomieniu programu debuger przerwie jego pracę praktycznie na samym początku, gdyż procedura InitializeComponent jest wywoływana w celu aktywacji komponentów na formularzu. Następnie z menu Run należy wybrać Inspect (opcja ta jest nieaktywna, gdy program nie jest uruchomiony). W efekcie zostanie wyświetlone okno dialogowe inspekcji — należy wpisać w polu Expression wartość Self i kliknąć przycisk OK.

W oknie dialogowym inspekcji należy podać nazwę klasy bądź innej struktury, której informacje chcemy wyświetlić. Procedura InitializeComponent jest umieszczona wewnątrz głównej klasy formularza, zatem wpisanie wartości Self spowoduje analizę głównego formularza. Rezultatem ustawienia punktu przerwań poza obiektem klasy jest wyświetlenie komunikatu o błędzie.

Budowa okna

Okno inspektora śledzenia udostępnia wiele szczegółowych informacji na temat klasy. Zakładki Properties oraz Methods są wyświetlane w przypadku inspekcji klasy.

Zakładka Methods wyszczególnia metody należące do klasy. Ponieważ lista jest dość długa, istnieje możliwość wyświetlania nazw metod należących jedynie do danej klasy (z pominięciem klas bazowych). W tym celu w menu kontekstowym należy usunąć zaznaczenie opcji Show Inherited.

Zakładka Properties wyświetla właściwości klasy, lecz aby uzyskać wartość danej właściwości, należy nacisnąć dodatkowy przycisk z ikoną znaku zapytania. Ikona ta pojawia się dopiero po zaznaczeniu konkretnej pozycji — np. @@WindowText@@. Wartość zostanie pobrana po naciśnięciu ikony (w moim przypadku TWinForm2).
Pierwsza zakładka, Data, wyświetla ogólne informacje związane z klasą.

Evaluate/Modify

Evaluate/Modify jest narzędziem umożliwiającym podgląd wybranej zmiennej w trakcie pracy programu (rysunek 9.12).

9.12.jpg
Rysunek 9.12. Okno Evaluate/Modify

Okno Evaluate/Modify może być wywołane jedynie w trakcie działania programu. Jeżeli program zatrzyma się w wyniku umieszczenia np. punktu przerwania, można z menu Run wybrać polecenie Evaluate/Modify.

Pasek narzędzi

Pasek narzędzi udostępnia kilka podstawowych opcji, które opisałem w tabeli 9.6.

Tabela 9.6. Przyciski paska narzędziowego okna Evaluate/Modify

Przycisk Opis
Evaluate Wykonuje wyrażenie z pola Expression.
Modify Umożliwia ewentualną zmianę wartości danej zmiennej. Pożądaną wartość należy wpisać w polu New Value.
Watch Umożliwia dodanie wybranej zmiennej do listy Watch List.
Inspect Otwiera okno inspektora śledzenia, w której znajduje się wartość danej zmiennej.
Help Wyświetla elektroniczną pomoc dotyczącą okna.

Korzystanie z okna Evaluate/Modify

Przed użyciem opcji Evaluate/Modify należy ustawić odpowiednie punkty przerwań. Załóżmy, że mamy prosty program:

1: program Project3;
2:
3: {$APPTYPE CONSOLE}
4: 
5: var
6:  S : String;
7:
8: begin
9:  Console.WriteLine('Nadaj wartość zmiennej S');
10:  S := Console.ReadLine;
11:
12:  Console.WriteLine('Zmieniona wartość zmiennej S:');
13:  Console.WriteLine(S);
14:  Console.ReadLine;
15: end.

Program ten pobiera wartość typu String, podaną przez użytkownika. Punkt przerwania należy umieścić w linii 12. Teraz można uruchomić program i wpisać na konsoli dowolną wartość. W tym momencie praca programu powinna zostać wstrzymana. Następnie z menu Run wybierzemy Evaluate/Modify, w polu Expression wpiszemy nazwę zmiennej — @@S@@, po czym można kliknąć przycisk Evaluate. Pole Result będzie zawierało wartość zmiennej. W przypadku aktywności kontrolki @@New Value@@ można nadać jej nową wartość.

Okno Call Stack

Okno Call Stack (stos wywołań) wyświetla listę wywoływanych procedur i funkcji. W celu otwarcia okna Call Stack należy z menu View wybrać Debug => Call Stack (rysunek 9.13).

9.13.jpg
Rysunek 9.13. Okno Call Stack

W projekcie WinForms na formularzu umieściłem komponent Button oraz wygenerowałem jego procedurę Click:

1:procedure TWinForm2.Button1_Click(sender: System.Object; e: System.EventArgs);
2:
3:  procedure SetValue(const Value : String);
4:  begin
5:    Button1.Text := Value;
6:  end;
7:
8: begin
9:  SetValue('Jakaś wartość');
10: end;

Punkty przerwań umieściłem w linii 9. oraz 5. Dla lepszego efektu umieściłem w procedurze zdarzeniowej drugą procedurę zagnieżdżoną — SetValue, która zmienia właściwość @@Text@@ komponentu. Po uruchomieniu programu i zatrzymaniu na linii 9. w oknie Call Stack zostaną wyświetlone procedury wywoływane w porządku wynikającym z kolejności ich wywołania.

Kliknięcie w polu Call Stack wybranej procedury powoduje przejście do jej deklaracji w kodzie źródłowym. Nieraz (w przypadku braku deklaracji) kliknięcie może spowodować otwarcie zakładki Disassembly, która służy do przedstawiania instrukcji w języku Asembler.

Okno Local Variables

Okno Local Variables (rysunek 9.14) jest podobne do Call Stack z tą różnicą, że prezentuje zawartość zmiennych lokalnych, a nie listę procedur i funkcji.

9.14.jpg
Rysunek 9.14. Okno Local Variables

Na rysunku 9.14 przedstawiłem okno Local Variables z zawartością wygenerowaną wskutek działania programu z poprzedniego przykładu. Lista rozwijalna u góry okna zawiera listę procedur. Po jej wybraniu na liście pojawia się lista parametrów lub zmiennych lokalnych danej procedury czy funkcji. Na przykład do procedury SetValue przekazujemy parametr @@Value@@ o wartości Jakaś wartość. Dodatkowo z poziomu tej procedury jest dostępna zmienna Self wskazująca na daną klasę.

Kliknięcie wybranej pozycji na liście spowoduje otwarcie okna inspektora śledzenia.

Okno Thread Status

Zacznę od wyjaśnienia, czym w rzeczywistości są wątki. Każda aplikacja (proces) działająca w systemie Windows posiada tzw. wątek główny (ang. primary thread), który może uruchamiać inne wątki poboczne (ang. secondary threads). W tym samym czasie może działać kilka wątków pobocznych, które wykonują różne lub te same operacje. Być może to, co napisałem do tej pory cokolwiek przybliżyło Czytelnikowi zasadę funkcjonowania wątków. Można sobie wyobrazić możliwość wykonywania innych czynności w tle aplikacji — bez jej blokowania. Użytkownik uzyskuje możliwość dokonywania zmian w programie, a w tle może działać inny wątek, który będzie wykonywał pozostałe operacje.

Procesem można nazwać każdą aplikację uruchomioną w danym momencie. Taką też terminologię będę stosował w dalszej części tego rozdziału. Zatem przyjmijmy, że proces jest egzemplarzem aplikacji, uruchomionym w systemie.

Podczas działania programu listę aktywnych wątków programu prezentuje okno Thread Status (rysunek 9.15).

9.15.jpg
Rysunek 9.15. Stan wątków procesu

Okno prezentujące stan wątków procesu powinno być zawsze na wierzchu w czasie działania programu. Jeżeli jednak tak nie jest, można je wywołać z menu View => Debug Window => Threads. Okno dzieli się na kilka kolumn:

*Thread Id — identyfikator wątku,
*State — stan procesu (Init — inicjalizacja, Stopped — zatrzymany lub Runnable — w trakcie działania),
*Status — status wątku (może być np. Breakpoint),
*Location — plik źródłowy, w którym znajduje się wątek lub adres w pamięci.

Z poziomu menu kontekstowego okna jest możliwe również tzw. zabicie wątku (polecenie Terminate Process).

W niniejszej książce nie będę się zajmował tematem programowania wielowątkowego. W celu uzyskania szerszych informacji na temat tworzenia wątków w Delphi odsyłam do pomocy elektronicznej Delphi lub do ósmego rozdziału książki Delphi 7. Kompendium programisty, wydanej nakładem wydawnictwa Helion w 2003 roku.

Okno Event Log

Kronika zdarzeń (ang. Event log) jest oknem służącym do rejestracji rozmaitych zdarzeń związanych z danym procesem — start, zatrzymanie, wysyłanie komunikatów i inne podobne zdarzenia. Kronika zdarzenia powinna być widoczna w trakcie działania programu. Jeżeli tak nie jest, z menu View należy wybrać Debug Window => Event Log (rysunek 9.16).

9.16.jpg
Rysunek 9.16. Kronika zdarzenia

Menu kontekstowe kroniki śledzenia

Co prawda, menu kontekstowe kroniki śledzenia nie jest zbyt zaawansowane, aczkolwiek umożliwia dokonanie kilku podstawowych czynności — jak np. wyczyszczenie kroniki lub zapisanie jej do pliku czy dodanie własnego komentarza. Tabela 9.7 prezentuje elementy menu kontekstowego.

Tabela 9.7. Elementy menu kontekstowego kroniki śledzenia

Element Opis
Clear Events Wybranie opcji czyści zawartość kroniki. Uwaga! Czyszczenie odbywa się bez potwierdzenia, trzeba więc uważać, aby przez przypadek nie skasować zawartości kroniki.
Save Events to File Umożliwia zapisanie całej kroniki do wybranego pliku tekstowego.
Add Comment Wyświetla okno, które umożliwia dodanie własnego komentarza do kroniki.
Properties Wyświetla okno właściwości kroniki — opowiem o nim w dalszej części rozdziału.
Stay On Top Zaznaczenie tej opcji spowoduje, iż okno przez cały czas będzie widoczne.

Kopiowanie zawartości do schowka

Menu kontekstowe nie zawiera żadnych opcji pozwalających na skopiowanie zawartości do schowka, postanowiłem więc wspomnieć także o tym.
Nawigowanie po kolejnych wierszach w oknie kroniki jest możliwe za pomocą klawiszy kursora. Skopiowanie zaznaczonej linii do schowka jest możliwe po zastosowaniu kombinacji Ctrl+C. W celu zachowania całej zawartości najpierw należy zaznaczyć całość (Ctrl+A), a dopiero później skopiować.

Dodatkowo, zaznaczanie konkretnych linii jest możliwe przez przytrzymanie klawisza Ctrl lub Shift z równoczesnym klikaniem wybranych elementów (linie) kroniki.

Okno modułów

Okno modułów (ang. modules) uaktywnia się po wybraniu z menu View polecenia Debug Window/Modules. Zawartość okna jest przedstawiona na nowej zakładce w edytorze kodu (rysunek 9.17).

9.17.jpg
Rysunek 9.17. Okno modułów

Okno podglądu modułów daje informacje o aktualnie załadowanych bibliotekach wraz ze ścieżkami do nich oraz adresem w pamięci. Dodatkowo, po prawej stronie okna mechanizm reflection wyświetla drzewiastą, hierarchiczną strukturę naszej aplikacji.

W menu kontekstowym znajdują się dwie pozycje. Pierwszą jest Browse Class, która powoduje otwarcie nowej zakładki w edytorze kodu oraz uruchomienie mechanizmu reflection w celu dokonania analizy klasy. Druga pozycja umożliwia ewentualną edycję kodu źródłowego (Edit Source).

Okno deasemblacji

Oficjalnie okno deasemblacji weszło skład debugera dopiero w Delphi 4, lecz już Delphi 2 umożliwiało wyświetlanie instrukcji programu w formie języka maszynowego. Okno deasemblacji jest pokazane na rysunku 9.18.

9.18.jpg
Rysunek 9.18. Okno deasemblacji

W celu wywołania okna deasemblacji z menu View należy wybrać Debug Window => CPU. Jest to narzędzie dla bardziej zaawansowanych programistów i wymaga podstawowej znajomości Asemblera, zatem nie będziemy tutaj szerzej omawiać tego tematu.

Polecenie Go to Address

Polecenie Go to Address jest zaawansowaną opcją, z której Czytelnik być może nigdy nie będzie korzystał. Jest to niewątpliwie narzędzie służące do wykrywania błędów, lecz jego stosowanie również wymaga znajomości języka Asembler.

Istotą działania polecenia Go to Address (menu kontekstowe edytora kodu, pozycja Debug => Go to Address) jest odnajdywanie i wyświetlenie w oknie deasemblacji konkretnego elementu pamięci komputera. Poszukiwany adres musi być podany w formie przyjętej dla kompilatora JIT, czyli np. @($3,$60005C4,$62). Debuger na podstawie adresu spróbuje zlokalizować (powiązać ze zmienną) błąd w programie i, jeżeli mu się to uda, zaznaczy konkretną linię.

Okno Message View

Okno Message View jest zapewne znane Czytelnikowi. Jest to okno położone u dołu interfejsu Delphi, służące do wyświetlania ostrzeżeń oraz informacji o błędach Delphi. Opcje menu kontekstowego nie są zbyt skomplikowane — umożliwiają np. zapisanie wszystkich komunikatów błędu w pliku (SaveAll Messages). Należy zwrócić uwagę, że w menu kontekstowym są dwie opcje: Save Messages oraz Save All Messages. Wydawać by się mogło, że pierwsza z nich zapisuje tylko zaznaczoną linię tekstu, a druga — cały tekst komunikatu. Trudno powiedzieć, dlaczego obie działają niemal identycznie. Druga z nich zapisuje jedynie informacje o nazwie zakładki, na której znalazły się błędy.

Standardowo komunikaty o błędach są wyświetlane w oknie Message View na zakładce Build. Niektórzy mogą nie wiedzieć, że owe okno ma jeszcze inne zastosowania. Przykładowo, w trakcie projektowania stron WWW z wykorzystaniem edytora WYSIWYG Delphi nowa zakładka HTML View Errors w oknie Message View może zawierać informacje o błędach kodu HTML.

Zasada kopiowania poszczególnych linii lub całości jest identyczna jak w przypadku okna Event Log.

Praca krokowa

Praca krokowa jest sposobem pracy, bardzo często wykorzystywanym przez programistów. Dzięki temu program może być wykonywany instrukcja po instrukcji, co pozwala na lepsze rozpoznanie i wykrycie ewentualnego błędu.

Uruchamianiem programu w trybie pracy krokowej steruje się za pomocą paska narzędziowego Debug, a konkretnie — z wykorzystaniem dwóch przycisków: Trace into (F7) i Step over (F8).

Ikony na gutterze

Łatwo zauważyć niebieskie punkty, które pojawiają się na gutterze po zakończeniu kompilacji. Punkty te identyfikują linie kodu źródłowego, którym proces kompilacji przyporządkowuje kod maszynowy. Być może Czytelnik zwrócił uwagę także na to, że nie wszystkie linie są oznaczone w ten sposób. Jest to związane częściowo z optymalizacją — spójrzmy na poniższy kod:

1: program Project3;
2: 
3: {$APPTYPE CONSOLE}
4: 
5: var
6:  I : Integer;
7:  X : Integer;
8: 
9: begin
10:   X := 10;
11: 
12:   { komentarz }
13: 
14:  Console.WriteLine('Rozpoczynamy pracę krokową...');
15:  for I := 0 to 10 do
16:    Procc;
17: 
18:   Console.WriteLine('Ostatnia instrukcja...');
19:   Console.WriteLine('Naciśnij Enter, aby zamknąć...');
20:   Console.ReadLine;
21: end.

Po skompilowaniu programu niebieskie punkty nie pojawią się obok linii oznaczających deklarację zmiennych:

var
  I : Integer;
  X : Integer;

Nie pojawiają się także przy komentarzach oraz liniach pustych — one także są usuwane przez kompilator.

Ciekawostką fakt, że niebieski punkt nie pojawi się również przy linii 16.:

X := 10;

Dlaczego? Związane jest to z optymalizacją kodu. Warto zwrócić uwagę, że nigdzie w kodzie zmienna @@X@@ nie jest wykorzystywana. Kompilator potrafi to określić i ignoruje to wywołanie.

Przekraczanie i wkraczanie

Teraz mogę powrócić już do wyjaśniania zasad pracy krokowej kompilatora. Łatwo zauważyć, że na pasku narzędziowym Debug istnieją dwa przyciski do tego przeznaczone — Trace Into (skrót F7) oraz Step Over (skrót F8). Przekraczanie (Step Over) oznacza wykonywanie kodu źródłowego linia po linii, lecz z pominięciem procedur i funkcji. Oznacza to, iż w razie napotkania odwołania do funkcji, debuger nie będzie analizował kodu w ciele tych procedur. Funkcja Trace Into różni się właśnie tym, że debuger wkracza i wykonuje pracę krokową również w ciele procedur i funkcji. Jeżeli dana procedura znajduje się w innym module, to Delphi otworzy w edytorze kod źródłowy tego modułu i tam rozpocznie pracę krokową. Na listingu 9.2 zaprezentowałem program, na którym będziemy testowali mechanizm pracy krokowej.

Listing 9.2. Przykładowy program prezentujący mechanizm pracy krokowej

program P9_2;

{$APPTYPE CONSOLE}


  procedure Procc;
  begin
    Console.WriteLine('Wywołanie procedury Procc');
  end;

var
  I : Integer;
  X : Integer;

begin
  X := 10;

  { komentarz }

  Console.WriteLine('Rozpoczynamy pracę krokową...');
  for I := 0 to 10 do
    Procc;

  Console.WriteLine('Ostatnia instrukcja...');
  Console.WriteLine('Naciśnij Enter, aby zamknąć...');
  Console.ReadLine;
end.

Zwróćmy uwagę, że pętla for wykonuje 11 razy procedurę Procc. Teraz można uruchomić program klawiszem F8. Pierwsza linia, na której debuger wstrzyma pracę, to blok begin rozpoczynający działanie aplikacji. Każde następne naciśnięcie F8 spowoduje przejście do kolejnej instrukcji. Zauważmy jednak, że przy pracy krokowej jest pomijana linia, w której do zmiennej @@X@@ przypisano wartość (dalej nie ma jednak odwołania do zmiennej @@X@@).

Uruchomienie programu za pomocą klawisza F7 spowoduje, iż debuger zatrzyma się również w ciele procedury Procc.

Powrót do normalnej pracy programu nastąpi z chwilą naciśnięcia klawisza F9 lub wybrania polecenia Run z menu Run.

Istotny jest fakt, że praca krokowa, tak samo jak punkty przerwań, powoduje zatrzymanie pracy programu. Pozwala to na wykorzystywanie narzędzi oferowanych przez debuger, takich jak wyrażenia testowe czy podgląd zmiennych lokalnych. Warto się więc zastanowić, czy w czasie testowania aplikacji lepiej będzie używać punktów przerwań czy może pracy krokowej?

Opcje debugera

Należy rozróżnić dwa rodzaje opcji: opcje na poziomie projektu oraz na poziomie środowiska. Te pierwsze, ustawiane przy użyciu polecenia Project => Options, były omawiane już wcześniej. Teraz nadszedł czas, aby przedstawić opcje na poziomie środowiska, dostępne w menu Tools => Options. Znajdują się one w kategorii Debugger Options i jej podkategoriach.

Domyślnie jedyną zaznaczoną na zakładce Debugger Options opcją jest Integrated Debugging. Zalecam pozostawić tę opcję na uaktywnioną, dzięki czemu Delphi pozostanie zintegrowane z debugerem.

Opcja Map TD32 keystrokes on run powoduje ustawienie kombinacji klawiszy zgodnie z zewnętrznym programem Turbo Debugger firmy Borland. Ta opcja jest istotna jedynie dla osób, które sporo czasu spędziły z tym programem i są przyzwyczajone do stosowanych w nim skrótów klawiaturowych.

Mark buffers read-only on run powoduje ustawienie edytora kodu w trybie tylko do odczytu, dzięki czemu niemożliwe jest modyfikowanie kodu w trakcie działania debugera. Zalecam, aby nie zaznaczać tej opcji, gdyż możliwość wprowadzenia poprawek w kodzie w czasie, gdy debuger pracuje zawsze może się okazać przydatna.
Pozycja Rearrange editor local menu on run grupuje polecenia w menu kontekstowym edytora kodu, tak aby zapewnić łatwiejszy dostęp do interesujących opcji na poziomie debugowania.

Strona Borland Debuggers

Opcje z zakładki Borland Debuggers są związane (rysunek 9.19) jedynie z IDE debugera.

9.19.jpg
Rysunek 9.19. Zakładka Borland .NET Debugger

Do ciekawych opcji można zaliczyć Inspectors stay on top. Dzięki temu okna debugera będą widoczne nawet wtedy, jeżeli pierwotnie były nieaktywne.

Wszystkie opcje z kategorii Inspectors Defaults są związane z inspektorem śledzenia i określają jego domyślnie opcje. Na przykład Show inherited nakazuje wyświetlanie w oknie inspektora śledzenia metod i właściwości dziedziczonych po danej klasie. Sort by name powoduje domyślne sortowanie alfabetyczne, natomiast Show fully qualifield names pokazuje w oknie inspektora śledzenia pełne nazwy wraz z nazwą przestrzeni nazw, w której umiejscowiona jest dana właściwość.

Dzięki ostatniej liście rozwijalnej Debug Symbols Search Path możemy wskazać katalog, w którym debuger Delphi umiejscowi swoje pliki *.pdb. Domyślnie jest to katalog z projektem.

W Delphi dla .NET, oprócz pliku wykonywalnego (.exe), w katalogu zostanie utworzony także plik z rozszerzeniem *.pdb, który zawiera informację dla debugera. Istnieje możliwość wyłączenia generowania pliku *.pdb. Wystarczy otworzyć menu Project/Options, a następnie zaznaczyć kategorię Linker. Znajduje tam się pole Generate .PDB debug info file, z którego należy usunąć zaznaczenie.

Zakładka Language Exceptions

Zakładka ta jest związana jedynie z wyjątkami oraz z ich obsługą przez debuger. Lista zajmująca przeważającą część okna to lista wyjątków, które mają być ignorowane przez debuger (jeżeli opcja jest zaznaczona). Istnieje możliwość dodania własnego typu wyjątku ignorowanego poprzez przycisk Add, w którym należy podać nazwę wyjątku.

W sumie najważniejszą (przynajmniej dla mnie) opcją na tej zakładce jest Notify on language exceptions (we wcześniejszych wersjach Delphi to pole nosiło nazwę Stop on language exceptions). Jej zaznaczenie spowoduje, że wszelkie błędy tworzonej aplikacji będą obsługiwane przez debuger. Ja osobiście wyłączam tę opcję, powierzając obsługę błędów własnej aplikacji.

Zakładka Native OS Exceptions

Zakładka Native OS Exceptions jest nowością w Delphi 2005. Zawiera bardziej zaawansowane opcje, związane z obsługą wyjątków systemowych. Umożliwia określenie, czy błędy systemowe mają być obsługiwane przez samą aplikację, czy debuger Delphi. Centralną część okna zajmuje lista wyjątków. Poniżej można wybrać, czy wyjątek ma zostać obsłużony przez naszą aplikację (User program), czy też debuger (Debugger). Opcje na tej zakładce są przeznaczone dla bardziej zaawansowanych programistów, nie ma potrzeby zmieniania w niej czegokolwiek.

Zakładka Event Log

O właściwościach okna kartoteki zdarzeń (Event Log) wspominałem już wcześniej. Można uzyskać do niej dostęp po wybraniu z menu kontekstowego polecenia Properties. W wielu przypadkach w Delphi istnieje kilka dróg do tej samej opcji. Tak jest i teraz — te same ustawienia Event Log można bowiem znaleźć w kategorii Debugger Options (rysunek 9.20).

9.20.jpg
Rysunek 9.20. Zakładka Event Log

Opcje z kategorii Messages (domyślnie wszystkie są zaznaczone) dają możliwość wyboru rodzaju komunikatów, które trafiają do dziennika.

W sekcji General istnieje możliwość określenia, czy dziennik ma być czyszczony zaraz po starcie programu (Clear log on run) oraz maksymalnej długości dziennika (domyślnie opcja Unlimited length daje nieograniczoną długość). Ostatnia z opcji, Display process info with event, nakazuje dodanie do dziennika również informacji o procesie.

Ostatnia sekcja daje możliwość wyboru kolorów dla poszczególnych linii w dzienniku zdarzeń. Jest to raczej opcja kosmetyczna, która może zadowolić najbardziej wybrednych użytkowników. Powiem szczerze, że ja osobiście nigdy nie ingerowałem w domyślnie ustawienia Delphi związane z kolorystyką.

Menu związane z debugerem

Czytelnik na pewno dobrze już poznał menu oraz opcje okien związanych z debugerem. Jako podsumowanie proponuję przeanalizowanie tabeli 9.8 i 9.9, w których jeszcze raz przedstawiam wszystkie te opcje menu.

Tabela 9.8. Elementy menu kontekstowego edytora kodu związane z debugerem

Element Skrót klawiaturowy Opis
Toggle Breakpoint F5 Ustawia lub likwiduje punkt przerwania w linii określonej kursorem.
Evaluate/Modify Ctrl+F7 Umożliwia podgląd i (lub) modyfikację zmiennych w trakcie działania programu.
Add Watch at Cursor Ctrl+F5 Dodaje zmienną zaznaczoną w edytorze do listy Watch List.
Inspect Alt+F5 Otwiera okno inspektora śledzenia.
Goto Address Umożliwia określenie w programie adresu, od którego wznowiony zostanie proces.
View Disassembly Wyświetla okno kodu w języku Asembler.
Run to Cursor Ctrl+F4 Wykonuje program do osiągnięcia linii, w której znajduje się kursor.

Tabela 9.9. Elementy menu Run

Element Skrót klawiaturowy Opis
Run F9 Uruchamia aplikację pod kontrolą debugera.
Run without debuging Uruchamia program bez kontrolki debugera.
Parameters Umożliwia nadanie parametrów, z jakimi program zostanie uruchomiony.
Load Process Ładuje proces, który ma zostać uruchomiony pod kontrolą debugera.
Attach to Process Umożliwia debugowanie programu znajdującego się na innym komputerze.
Step Over F8 Praca krokowa bez uwzględniania wnętrza procedur i funkcji.
Trace Into F7 Praca krokowa z uwzględnianiem wnętrza procedur i funkcji.
Trace to Next Source Line Shift+F7 W przypadku ustawienia punktu przerwania przechodzi do następnej instrukcji w kodzie.
Run to Cursor Ctrl+F4 Wykonuje program do osiągnięcia linii, w której znajduje się kursor.
Show Execution Point Wyświetla punkt wykonania programu w edytorze kodu. Jeżeli jest to konieczne, przewija zawartość do odpowiedniej linii.
Program Pause Wstrzymuje wykonywanie programu.
Program Reset Ctrl+F2 Bezwarunkowo zatrzymuje program.

Test

  1. Jaka jest maksymalna wartość, jaką może przyjmować zmienna typu Byte?
    a) 256,
    b) 255,
    c) 250.
  2. Jaki przełącznik odpowiada za sprawdzanie granic tablic czy ciągów znakowych?
    a) {$I+},
    b) {$R+},
    c) {$Q+}.
  3. Rejestry to:
    a) specjalna pamięć wbudowana w CPU,
    b) cała dostępna pamięć komputera,
    c) pamięć rezerwowana w trakcie uruchamiania programu.
  4. Za uzyskanie adresu komórki pamięci danej zmiennej odpowiada operator:
    a) ^,
    b) @,
    c) nie ma takiego operatora.
  5. Do alokowania pamięci w Delphi dla Win32 może posłużyć funkcja:
    a) New,
    b) GetMem,
    c) obie odpowiedzi są prawidłowe.
  6. Praca krokowa umożliwia:
    a) wstrzymanie działania programu w wyznaczonych miejscach,
    b) podgląd wartości zmiennych użytych w programie,
    c) obie odpowiedzi są prawidłowe.

FAQ

Czy istnieje prosty sposób pobierania wartości szesnastkowej i dziesiętnej za pomocą okna Watch List?

Tak. Po dodaniu zmiennej do okna wyrażeń testowych należy kliknąć ją dwukrotnie, co spowoduje otwarcie okna właściwości. W ramce poniżej należy zaznaczyć, w jakiej formie ma zostać przedstawiona dana liczba. Wartość szesnastkowa jest podawana po wybraniu opcji Hexedecimal, a dziesiętna — Decimal.

Czemu mój program działa tak wolno?

Być może przyczyną jest właśnie debuger. Łatwo to sprawdzić. Można z menu Run wybrać polecenie Run without debugging lub dezaktywować (polecenie Disable z menu kontekstowego) wyrażenia z okna Watch List. Jeżeli mimo to aplikacja działa wolno, to problem nie leży po stronie debugera, a zwyczajnie — jest spowodowany właściwościami samej aplikacji. Być może kod programu nie jest wystarczająco zoptymalizowany albo w miarę rozrastania się aplikacji parametry komputera okazały się za słabe (np. mało pamięci RAM).

Podsumowanie

W Delphi 2005 debuger jest potężnym narzędziem, wykorzystywanym przy pracy nad większymi projektami. Mimo że na razie wiedza ta może wydać się Czytelnikowi niepotrzebna, w przyszłości zapewne większość osób będzie wykorzystywać te elementy środowiska Delphi. Po lekturze tego rozdziału Czytelnik powinien znać funkcje oraz opcje debugera Delphi.

[[Delphi/Vademecum|Spis treści]]

[[Delphi/Vademecum/Prawa autorskie|©]] Helion 2005. Autor: Adam Boduch. Zabrania się rozpowszechniania tego tekstu bez zgody autora.

0 komentarzy