Rozdzia³ 3. ¨ Klasy i programowanie zorientowane obiektowo 127
Klasy stanowią główny temat tego rozdziału. Na nich właśnie opiera się Object Pascal oraz VCL. Stanowią one fundament wielu nowoczesnych języków programowania. Dowiesz się, co to są klasy i jak ich używać. Nieznane dotąd terminy, takie jak: obiekty, dziedziczenie i hermetyzacja przestaną być dla Ciebie tajemnicą. Na początek jednak poznasz jeszcze kilka elementów Object Pascala, o których nie wspomniałem w poprzednim rozdziale.
Zbiory są bardzo często wykorzystywane w Delphi, musisz więc wiedzieć, czym są i jak się ich używa.
Zbiór (typ mnogościowy) jest zestawem wartości jednego typu.
Nie jest ta definicja zbyt obrazowa. Najlepiej więc będzie, jeśli jak zwykle przytoczę jakiś przykład. Niech będzie nim właściwość Style obiektu VCL o nazwie Font. Może ona przyjmować następujące wartości:
u fsBold
u fsItalic
u fsUnderline
u fsStrikeout
Czcionka (Font) może posiadać jeden lub więcej z powyższych stylów, może też nie mieć żadnego z nich albo mieć je wszystkie. Jest to właśnie zbiór stylów.
Jak używa się zbiorów? Niech za przykład posłuży ponownie właściwość Style. Zwykle wybierasz pewne wartości ze zbioru stylów w czasie projektowania aplikacji. Czasem jednak zachodzi potrzeba zmiany tego zestawu w czasie wykonywania programu. Powiedzmy, że chcesz nadać czcionce dodatkowe atrybuty Bold i Italic. Jednym ze sposobów jest zadeklarowanie zmiennej typu TFontStyles i dodanie do zbioru stylów fsBold i fsItalic:
var
Styles : TFontStyles;
begin
Styles := Styles + [fsBold, fsItalic];
end;
Powyższy kod dodaje elementy fsBold i fsItalic do zbioru Styles. Nawiasy kwadratowe oznaczają, że chodzi o elementy zbioru. Użyte w ten sposób nazywane są konstruktorem zbioru. Kod ten nie zmienia stylu czcionki, na razie do zbioru dodane są tylko odpowiednie elementy. Aby rzeczywiście zmienić styl czcionki, należy nowo stworzony zbiór przypisać właściwości Font.Style odpowiedniego komponentu:
Memo.Font.Style := Styles;
Teraz chcemy na przykład, żeby czcionka ta nie miała dłużej atrybutu Italic. Trzeba więc usunąć ten styl ze zbioru:
Styles := Styles – [fsItalic];
Często trzeba sprawdzić, czy określona wartość jest już w zbiorze. Powiedzmy, że chcesz wiedzieć, czy czcionka posiada już atrybut Bold. Sprawdzenie, czy element fsBold jest w zbiorze Styles, wygląda następująco:
if fsBold in Styles then
JakasProcedura;
Czasami też zachodzi potrzeba upewnienia się, że zbiór przed przypisaniem nie posiada wcześniej żadnych wartości. Można to zrobić przypisując mu „zbiór pusty”:
{opróżnienie zbioru}
Styles := [];
{teraz dodanie atrybutów Bold i Italic}
W powyższym przykładzie kasowana jest cała zawartość zbioru, następnie dodawane są do niego dwa elementy. To samo można osiągnąć przypisując zbiorowi wprost te wartości:
Styles := [fsBold, fsItalic];
Żeby zmienić styl czcionki jakiegoś komponentu, nie potrzeba właściwie deklarować w tym celu osobnej zmiennej, można się do odpowiednich właściwości odwoływać bezpośrednio:
Memo.Font.Style := [];
Memo.Font.Style := Memo.Font.Style + [fsBold, fsItalic];
Zbiory są deklarowane przy pomocy słowa kluczowego set. Właściwość FontStyles jest zadeklarowana w pliku źródłowym (GRAPHICS.PAS) biblioteki VCL następująco:
TFontStyle = (fsBold, fsItalic, fsUnderline, fsStrikeout);
TFontStyles = set of TFontStyle;
W pierwszej linii zadeklarowany jest typ wyliczeniowy TFontStyle (typ wyliczeniowy jest listą możliwych wartości), w drugiej natomiast deklarowany jest zbiór TFontStyles.
Dzięki rzutowaniu typów kompilator może traktować typy danych tak, jakby były one innymi typami danych.
Oto przykład traktowania typu Char tak, jak typu Integer:
Litera : Char;
Liczba : Integer;
Litera := 'A';
Liczba := Integer(Litera);
Label1.Caption := IntToStr(Liczba);
Wyrażenie Integer(Litera) instruuje kompilator, żeby przekonwertował wartość typu Char na wartość typu Integer. Rzutowanie to jest niezbędne, ponieważ przypisywanie wartości typu Char zmiennej typu Integer nie jest dozwolone. Jeżeli spróbowałbyś dokonać tego przypisania bez rzutowania, kompilator wygenerowałby komunikat błędu Incompatible types: 'Integer' and 'Char'.
Po wykonaniu się powyższego przykładu na etykiecie pojawiłby się napis 65 (litera A ma w kodzie ASCII numer 65).
Rzutowanie typów nie zawsze jest jednak możliwe do wykonania. Popatrz na poniższy przykład:
procedure TForm1.Button1Click(Sender: TObject);
Pi : Double;
Pi := 3.14;
Liczba := Integer(Pi);
W tym przypadku próbuję przekonwertować liczbę Double na liczbę Integer. Nie jest to dozwolone, więc kompilator wypisze komunikat Invalid typecast. Do konwersji liczb zmiennoprzecinkowych na całkowite służą funkcje Trunc, Floor albo Ceil (obcięcie części ułamkowej, zaokrąglenie do dołu lub do góry).
Wskaźniki mogą być konwertowane z jednego typu na inny z użyciem operatora as (szczegóły o wskaźnikach oraz o operatorach is i as - w następnych rozdziałach).
Wskaźniki należą do tych elementów Object Pascala, których opanowanie sprawia najwięcej kłopotów. Są to zmienne, które przechowują adresy innych zmiennych w pamięci komputera; mówi się o nich, że „wskazują” na te zmienne.
Wskaźnik jest to zmienna, która zawiera adres innej zmiennej.
Powiedzmy, że stworzyłeś jakiś rekord, oraz że chcesz przekazać adres tego rekordu do procedury wymagającej wskaźnika jako swego parametru. Adres tej zmiennej rekordowej można uzyskać za pomocą operatora @:
Karta001 : TKartaAdresowa;
Wsk : Pointer;
{wpisanie wartości do pól rekordu}
...
Wsk := @Karta001;
FunkcjaWymagajacaWskaznika(Wsk);
Zmienna Wsk (typu Pointer) użyta jest do przechowywania adresu rekordu Karta001. Ten typ wskaźnika nazywany jest wskaźnikiem amorficznym (ang. untyped pointer), ponieważ nieokreślony jest typ zmiennej, na którą wskazuje. Jest to po prostu adres w pamięci. Innym rodzajem wskaźnika jest wskaźnik wskazujący na zmienną typu określonego przy deklarowaniu tego wskaźnika. Stworzyć można na przykład wskaźnik do rekordu TKartaAdresowa. Deklaracja mogłaby mieć postać:
type
PKartaAdresowa = ^TKartaAdresowa;
TKartaAdresowa = record
Imie : string;
Nazwisko : string;
Ulica : string;
Miasto : string;
KodPocztowy : Integer;
Typ PKartaAdresowa jest tu zadeklarowany jako wskaźnik na rekord TKartaAdresowa. Często w praktyce będziesz spotykał rekordy i wskaźniki deklarowane w ten właśnie sposób. Ale, tak na dobrą sprawę, do czego te wskaźniki tak naprawdę są potrzebne? Odpowiedź na to pytanie znajdziesz w następnych akapitach książki.
Długie łańcuchy (long strings) są bardzo rzadko używane w rekordach, ponieważ mają one nieokreśloną długość. Stała wielkość rekordu jest ważna przy zapisywaniu go na dysk. Używam więc raczej w rekordach łańcuchów shortstring lub tablic znaków.
W poprzednim rozdziale, przy omawianiu rekordów, przytaczałem kilka przykładów. We wszystkich tych przykładach używałem zmiennych statycznych. Określenie to oznacza, że pamięć dla tych zmiennych przydzielana była w obszarze stosu programu.
Alokacja statyczna oznacza przydzielanie pamięci dla zmiennych w obszarze stosu programu.
Stos jest obszarem pamięci operacyjnej rezerwowanej dla programu w czasie jego uruchamiania.
Cała pamięć potrzebna na zmienne lokalne, wywołania funkcji itd. pochodzi ze stosu programu. Pamięć ta jest alokowana zwykle w momencie startu programu lub w momencie wejścia do podprogramu. Pamięć potrzebna na zmienne lokalne procedury lub funkcji przydzielana jest w momencie wejścia do tej procedury lub funkcji, zwalniana jest natomiast w momencie wyjścia z niej. Analogicznie, w momencie zakończenia programu zwalniana jest cała przydzielona uprzednio dla niego pamięć. Wszystko to odbywa się w pełni automatycznie.
Alokacja statyczna ma swoje wady i zalety. Zaletą jej jest duża szybkość. Minusem zaś jest to, że rozmiar stosu programu nie może być w żaden sposób zmieniony w czasie jego działania. Jeżeli program przekroczy obszar stosu, może zdarzyć się praktycznie wszystko. Program może się zawiesić, może się zacząć dziwnie zachowywać, może działać z pozoru normalnie i zawiesić się w momencie zakończenia. Problem ten ma mniejsze znaczenie w środowisku 32-bitowym niż w 16-bitowym, jednak ciągle istnieje.
Dla zmiennych o typach wbudowanych i dla małych tablic jest to całkowicie wystarczające rozwiązanie. Gdy jednak planujesz używać np. dużych tablic rekordów, dynamiczna alokacja pamięci ze sterty (ang. heap) może okazać się jedynym rozwiązaniem. Sterta oznacza całą wolną pamięć fizyczną komputera plus całą wolną przestrzeń na dysku twardym. W typowym systemie Windows jest to zwykle około 100 MB. Można więc przyjąć, że dostępna pamięć wirtualna jest praktycznie nieograniczona. Okupione jest to jednak pewnym narzutem czasowym na dynamiczny przydział pamięci, zapis i odczyt z dysku itd. W większości przypadków jednak ten narzut jest niezauważalny. Kolejną niedogodnością jest fakt, że przydział dynamiczny pamięci nie odbywa się automatycznie – programista musi o to zadbać sam.
Alokacja dynamiczna oznacza przydzielanie pamięci dla zmiennych w obszarze sterty.
Sterta jest to cała dostępna pamięć wirtualna komputera.
W programie napisanym w Object Pascalu, dynamiczny przydział pamięci może odbywać się na kilka sposobów. Najlepszym, wydaje się, z nich jest użycie funkcji AllocMem. Funkcja ta przydziela pamięć i wypełnia ją zerami (inne metody to użycie procedury GetMem i funkcji New). Wróćmy jeszcze raz do przykładu z rekordami zawierającymi dane adresowe. Poprzednio, pamięć przydzielana była dla każdego z rekordów statycznie:
b.senni