z przyk│adami w Delphi

WstΩp

Tytu│ tego artyku│u mo┐e siΩ wydawaµ co najmniej dziwny. Dlatego ju┐ na wstΩpie spieszΩ z wyja╢nieniem, o co chodzi. Podczas codziennej praktyki programistycznej zawsze trzeba sobie jako╢ organizowaµ r≤┐ne struktury danych i jak najoptymalniej zapisywaµ je w pamiΩci. Wiele (nazwijmy to na razie tak og≤lnie) element≤w w Windows, a w Delphi w szczeg≤lno╢ci, posiada wbudowan▒ mo┐liwo╢µ przechowywania warto╢ci 32-bitowej. Tak▒ liczbΩ mo┐na te┐ sobie oczywi╢cie, podobnie, jak ka┐d▒ inn▒ dan▒ samemu przechowywaµ w zdefiniowanej przez siebie zmiennej.

Dlaczego 32 bity?

Dlaczego w│a╢ciwie za temat obra│em w│a╢nie liczby 32-bitowe? Co w nich jest takiego szczeg≤lnego?

Po pierwsze: 32-bity to w│a╢nie tyle danych, na ile zoptymalizowane s▒ dzisiejsze 32-bitowe procesory (jak Pentium), oraz 32-bitowe systemy operacyjne (jak Windows). Dlatego jest to najbardziej optymalna ilo╢µ danych. Zar≤wno z punktu widzenia oszczΩdno╢ci czasu procesora, jak i pamiΩci. Je╢li deklarujesz zmienn▒ typu Byte czy Word, one i tak bΩd▒ przechowywane ka┐da w 4 bajtach (32 bitach), z kt≤rych wykorzystane bΩdzie tylko odpowiednio 1 i 2. Tak to robi procesor, tak to robi kompilator. Jest to podyktowane wzglΩdami optymalizacji – po prostu takie liczby szybciej siΩ przetwarza, zapisuje i odczytuje.

Po drugie: m≤wi▒c liczba mam na my╢li jedynie jej typ docelowy. Przecie┐ mo┐na w 32-bitach opisaµ tak┐e dowolne inne dane, innego typu. Co zreszt▒ bΩdziemy robili w dalszej czΩ╢ci artyku│u! Bardzo wiele r≤┐nego typu danych, jakich u┐ywa siΩ w Windows i og≤lnie w programowaniu zajmuje w│a╢nie 32 bity lub najlepiej jest je w│a╢nie tak zapisywaµ. Oto wiΩc widaµ jeszcze jeden pow≤d, dlaczego w│a╢nie 32 bity: poniewa┐ za pomoc▒ takiej w│a╢nie ilo╢ci danych mo┐na zapisywaµ najwiΩcej r≤┐nych rodzaj≤w danych przy jednoczesnej oszczΩdno╢ci pamiΩci (nic siΩ nie marnuje) i optymalnie szybkim czasie ich przetwarzania.

Typy 32-bitowe

W Delphi istnieje szereg typ≤w przechowuj▒cych ca│kowite liczby 32-bitowe. Typy Integer (ze znakiem) i Cardinal (bez znaku) maj▒ to do siebie, ┐e zajmuj▒ tyle bit≤w, ilu bitowy jest kompilator. W Turbo Pascalu dla DOS by│o to 16 bit≤w. W Delphi dla Windows jest ich 32. Inne typy, od zawsze 32-bitowe to Longint (ze znakiem) i LongWord (bez niego). Tak wiΩc w 32-bitowym Delphi (wersja 2 i nastΩpne) typy Integer i Longint s▒ identyczne. Zmienne tego typu mog▒ przechowywaµ warto╢ci z zakresu od –2`147`483`648 do +2`147`483`647. Osobi╢cie lubiΩ m≤wiµ na co dzie±, ┐e jest to zakres od oko│o minus 2 miliard≤w do oko│o plus 2 miliard≤w. Cardinal i LongWord za╢ od 0 do +4`294`967`295.

Rzutowanie typ≤w

Je╢li jakie╢ miejsce przeznaczone jest na przechowywanie warto╢ci 32-bitowej, mo┐emy wykorzystaµ j▒ w dowolny spos≤b. Niekonieczne zgodnie z typem, jaki zosta│ dla niej zadeklarowany. Je╢li to Ty sam deklarujesz zmienn▒, mo┐esz nadaµ jej taki typ, jakiego typu dane bΩdziesz tam przechowywa│. Ale je╢li takie miejsce ju┐ istnieje, a jego typ jest niezgodny z typem danych, jakie chcesz do niego zapisywaµ i z niego odczytywaµ? Wtedy u┐yj rzutowania typ≤w. W Delphi wygl▒da ono nastΩpuj▒co:

Zmienna_typu_1 := Typ_1(Zmienna_typu_2);

co oznacza, ┐e dane typu 2 „przerabiasz” na dane typu 1. Na czym owo przerabianie polega? Je╢li obydwa typy s▒ takiego samego rozmiaru (np. 32 bity), komputer po prostu przepisuje dane bit-po-bicie. Zupe│nie nie patrzy, co to za typy i jak one siΩ maj▒ do siebie. Nie pr≤buje przeprowadziµ ┐adnej konwersji. I o to w│a╢nie nam chodzi. Przecie┐ miejsce ma tylko przechowywaµ Twoje dane. A ┐e jest innego typu, ni┐ byµ powinno, to ju┐ nie Twoja wina. Kiedy bΩdziesz chcia│ odczytaµ dane z takiego miejsca, z powrotem przeprowadzisz rzutowanie typ≤w w drug▒ stronΩ – na typ docelowy.

S│≤w kilka o s│ownictwie

CiΩ┐ko jest pisaµ o programowaniu. Choµ mo┐na z │atwo╢ci▒ wyra┐aµ siΩ precyzyjnie, kiedy chce siΩ u┐yµ jakiego╢ s│owa og≤lnego pojawiaj▒ siΩ problemy. Wyrazy takie, jak sk│adnik, czyli inaczej komponent, a tak┐e obiekt i wiele innych maj▒ swoje precyzyjne znaczenia. Dlatego w artykule tym na ujΩcie pewnych zagadnie± og≤lnych bΩdΩ u┐ywa│ s│≤w miejsce lub element.

Do czego mo┐e siΩ przydaµ liczba 32-bitowa

Inaczej m≤wi▒c: jakie dane i jakiego typu mo┐na zapisaµ (czasem po koniecznym rzutowaniu typ≤w) w 32 bitach? Poni┐ej przedstawiam najczΩ╢ciej spotykane w praktyce programistycznej zastosowania.

Indeks

32-bitow▒ liczbΩ ze znakiem typu Integer lub Longint mo┐na u┐yµ jako indeksu, czyli do oznaczania kolejnych numer≤w jaki╢ element≤w. Zakres, jaki ten typ udostΩpnia jest wystarczaj▒co du┐y, ┐eby ponumerowaµ dowoln▒ ilo╢µ znajduj▒cych siΩ w pamiΩci RAM obiekt≤w. Rozpatrzmy przyk│ad: Choµby ka┐dy numerowany element sk│ada│ siΩ tylko ze swojego indeksu, bez ┐adnych dodatkowych danych. Aby wyczerpa│a siΩ pula wszystkich dodatnich indeks≤w, obiekty te musia│yby zaj▒µ w sumie 4 * 2`147`483`647 = 8`589`934`592 B, czyli 8 GB. A kto ma tyle pamiΩci RAM? Dlatego zastosowanie liczby 32-bitowej do przechowywania indeks≤w jaki╢ element≤w, czyli nadawania im kolejnych numer≤w wydaje siΩ byµ rozwi▒zaniem wrΩcz idealnym. I to nawet, je╢li „zmarnujΩ siΩ” ca│a ujemna po│owa dostΩpnego zakresu warto╢ci.

Licznik

Innym zastosowaniem liczby 32-bitowej jest u┐ycie jej w roli licznika. Dok│adnie chodzi o to, ┐eby liczba taka by│a w ka┐dym cyklu jakiego╢ algorytmu zwiΩkszana (lub zmniejszana), zwykle o 1. Jakie ma to zastosowanie? Ogromne! Liczniki s▒ wszΩdzie! Oto przyk│ad: W ka┐dym cyklu zwiΩkszasz warto╢µ licznika o 1. Je╢li jaka╢ czΩ╢µ kodu ma byµ wykonywana co, powiedzmy, 7 cykl, napisz co╢ takiego:

if (Licznik mod 7) = 0 then ...

Oznacza to, ┐e je╢li reszta z dzielenia licznika przez 7 bΩdzie wynosi│a 0 (innymi s│owy licznik bΩdzie podzielny przez 7), dany kod ma zostaµ wykonany.

Pozostaje jeszcze jedno zagadnienie. Czy zastosowanie 32-bitowej liczby ze znakiem w roli licznika nie natrafi na problem w postaci wyczerpania siΩ zakresu? Owszem. Tutaj, w przeciwie±stwie do indeksu, takie zagro┐enie istnieje i, w zale┐no╢ci od konkretnego przypadku, mo┐e okazaµ siΩ ca│kiem realne. Je╢li licznik ma liczyµ tylko do okre╢lonej wielko╢ci, nie ma problemu. Wielko╢µ ta najprawdopodobniej nie bΩdzie wiΩksza ni┐ 2 mld. A co, je╢li licznik ten ma liczyµ w niesko±czono╢µ? To zale┐y... Je╢li np. na on byµ zwiΩkszany co sekundΩ, to ┐eby przekroczyµ zakres, bΩdzie na to potrzebowa│ w przybli┐eniu 68 lat! Co innego, je╢li licznik ten jest inkrementowany bardzo czΩsto, wiele tysiΩcy razy na sekundΩ, w dodatku przez stosunkowo d│ugi czas. A co bΩdzie, je╢li zakres siΩ sko±czy? C≤┐... Licznik powinien siΩ wyzerowaµ, ale niczego nie obiecujΩ. R≤┐nie to mo┐e byµ. Radzi│bym jednak nie zostawiaµ tak tego i co╢ z tym zrobiµ. Najprostszy ╢rodek zaradczy to wykrywanie przekroczenia zakresu:

if (Licznik = MAX_INT) then Licznik := 0;

Jeszcze pro╢ciej sprawa wygl▒da, je╢li licznik ma liczyµ tylko do ilu╢ tam, np. co╢ siΩ ma dziaµ jedynie co 7 cykl. W takim wypadku zamiast inkrementowaµ licznik w niesko±czono╢µ i ka┐dorazowo obliczaµ resztΩ z dzielenia, mo┐na zrobiµ co╢ takiego:

if Licznik = 6 {7-1} then
begin
{ Co╢, co ma siΩ zrobiµ }
Licznik := 0;
end;

Liczba

Dotychczas nie wspomnia│em jeszcze o najbardziej naturalnym, choµ wcale nie najprostszym zastosowaniu liczby 32-bitowej: jako po prostu liczbΩ. Liczby takie mog▒ siΩ przydaµ – i przydaj▒ siΩ czΩsto! Bardzo r≤┐ne jest ich zastosowanie. Tak samo r≤┐ne s▒ niestety wymagania, jakie takim liczbom siΩ stawia. W zale┐no╢ci od konkretnego zastosowania taka liczba mo┐e spokojnie mie╢ciµ siΩ w zakresie typu Integer, w innym przypadku za╢ przekraczaµ go o ca│e rzΩdy wielko╢ci. Je╢li masz pewno╢µ, ┐e liczba nie przekroczy warto╢ci MAX_INT w jedn▒ stronΩ, a –2`147`483`648 w drug▒ lub je╢li j▒ kontrolujesz, wszystko jest OK. Je╢li jednak zakres ten oka┐e siΩ niewystarczaj▒cy, zmuszony bΩdziesz zastosowaµ inne, „wysokobitowe” typy liczbowe: sta│oprzecinkowe Cardinal, Int64 lub zmiennoprzecinkowe Single, Double czy Comp. Ten ostatni nie przechowuje czΩ╢ci u│amkowej, jest wiΩc w rzeczywisto╢ci typem ca│kowitym. Zaliczany jest jednak ze wzglΩdu na FPU – koprocesor arytmetyczny, do typ≤w zmiennoprzecinkowych i jest z nimi kompatybilny podczas konwersji.

Je╢li potrzebny jest Ci odrobinΩ wiΩkszy zakres, ni┐ daje typ Integer (maksymalnie 2 razy), nie masz natomiast zamiaru u┐ywaµ liczb ujemnych, warto skorzystaµ z typ≤w Cardinal oraz Longword. S▒ to 32-bitowe typy ca│kowite (w│a╢ciwie trafniejszym okre╢leniem by│oby tutaj „naturalne”) bez znaku. Oznacza to, ┐e ich zakres wynosi od 0 do 4`294`967`295. Liczba taka jest r≤wnie┐ 32-bitowa. Co z tego wynika, nie trzeba chyba nikomu m≤wiµ? Mo┐na j▒ przechowywaµ w ka┐dym miejscu, gdzie do wykorzystania s▒ w│a╢nie 32 bity. Pojawia siΩ tu tylko jeden problem, je╢li miejsce, w kt≤rym tak▒ liczbΩ bΩdziesz przechowywaµ jest typu Integer. Chodzi o to, ┐e je╢li napiszesz co╢ takiego:

{ ¼LE!!! }
Liczba_Integer := Moja_liczba_Cardinal;

Komputer bΩdzie stara│ siΩ przekonwertowaµ j▒. Nie powinno byµ z tym problem≤w szczeg≤lnie, ┐e oba typy maj▒ podobn▒ budowΩ i dzia│anie. R≤┐ni▒ siΩ „tylko” jednym bitem. I w│a╢nie to jest ten bit sporny. Pomy╢l, co bΩdzie, je╢li Moja_Liczba_Cardinal nale┐y do g≤rnego zakresu powy┐ej MAX_INT, np. ma warto╢µ r≤wn▒ oko│o 4 mld? Komputer zaprotestuje – liczba konwertowana poza zakresem. A rozwi▒zanie jest takie:

Liczba_Integer := Integer(Moja_liczba_Cardinal);

Jak widaµ jest tu rzutowanie typ≤w. DziΩki takiemu zabiegowi komputer nie bΩdzie siΩ zastanawia│, ┐e ta liczba jest takiego typu, a ta zmienna takiego i pr≤bowa│ czegokolwiek konwertowaµ. I o to w│a╢nie chodzi. Je╢li liczbΩ typu Cardinal o warto╢ci 4 mld przypiszemy zmiennej typu Integer, jej warto╢µ bΩdzie wynosi│a jakie╢ -2 mld. Sk▒d to wynika? Ot≤┐ jeden z bit≤w jest w typie Integer przeznaczony na przechowywanie znaku (+/-), w typie Cardinal za╢, podobnie, jak wszystkie pozosta│e, zawiera warto╢µ bΩd▒c▒ sk│adow▒ liczby. Ale nic to nie szkodzi. Je╢li teraz odczytamy tak▒ liczbΩ typu Integer z powrotem jako typu Cardinal, otrzymamy nasz orygina│ w nienaruszonym stanie.

Moja_zmienna_Cardinal := Cardinal(Liczba_Integer);

Uchwyt

Byµ mo┐e nie ka┐da osoba programuj▒ca w Delphi wie, co to uchwyt (ang. Handle). Ale ka┐dy prawdziwy programista pisz▒cy w Windows wiedzieµ o tym powinien. Szczeg≤│owe wyja╢nienie tego pojΩcia nie le┐y niestety w zakresie tego artyku│u. Po szczeg≤│y odsy│am do wszelkiego rodzaju dokumentacji Win32API®. Og≤lnie rzecz bior▒c uchwyt to liczba – indeks. Windows nadaje takie indeksy unikatowe w danej chwili w skali ca│ego systemu znajduj▒cym siΩ w pamiΩci obiektom takim, jak okna, przyciski czy paski przewijania. Inne uchwyty mog▒ wskazywaµ na obiekty GDI takie, jak konteksty urz▒dzenia (DC – Device Context; odpowiednik Canvasa z Delphi), pi≤ra, pΩdzle i inne. Uchwytem pos│uguje siΩ program tak┐e podczas operacji na plikach i kluczach Rejestru oraz w wielu innych sytuacjach. Jak dzia│aj▒ uchwyty? Tworz▒c jaki╢ element (jak okno czy przycisk) b▒d╝ otwieraj▒c istniej▒cy (np. plik) otrzymujesz jego uchwyt. Od tej chwili podajesz go jako pierwszy parametr funkcji, kt≤re wywo│ujesz chc▒c wykonaµ jakie╢ operacje na tym elemencie. Potem musisz go zamkn▒µ lub usun▒µ.

Do reprezentacji uchwyt≤w istnieje wiele typ≤w. THANDLE, HWND, HINST to tylko niekt≤re z nich. Oto, jak typy takie zdefiniowane s▒ w kodzie ╝r≤d│owym modu│u Windows:

type
HICON = type LongWord;

I tak dla ka┐dego typu uchwytu. Czym jest typ LongWord? Jak ju┐ pisa│em wy┐ej, to 32-bitowy (a jak┐e!) typ ca│kowity bez znaku w 32-bitowym kompilatorze Delphi r≤wny Cardinal. Tak wiΩc do zapisania uchwytu idealnie nadaje siΩ 32-bitowa liczba.

Wska╝nik

Wska╝nik, jaki jest, ka┐dy widzi. Liczba oznaczaj▒ca numer kom≤rki w pamiΩci, gdzie co╢ jest zapisane. Dawno dawno temu, w epoce DOSa │upanego nie mieli╢my jeszcze p│askiego 32(a ile┐by mog│o byµ?) -bitowego modelu pamiΩci wirtualnej. PamiΩµ RAM by│a wtedy podzielona na strony. Obowi▒zywa│y 2 rodzaje wska╝nik≤w: tzw. bliskie (ang. near lub short) sk│adaj▒ce siΩ jedynie z numeru kom≤rki pamiΩci (w odniesieniu do jakiej╢ tam bie┐▒cej jej czΩ╢ci) 16-bitowe oraz tzw. dalekie (ang. far lub long), kt≤re sk│ada│y siΩ z tzw. adresu i offsetu, czyli odpowiednio numeru „strony” pamiΩci i numeru kom≤rki na tej stronie.

No, to tyle historii. Teraz nasta│y nam lepsze czasy. Mamy jednolite wska╝niki. A zajmuj▒ one (no, zgadnij?) – 32 bity! Tak jest! Dlatego w│a╢nie wska╝niki nadaj▒ siΩ r≤wnie┐ do przechowywania w miejscach przeznaczonych na liczby 32-bitowe. Powiem wiΩcej: jest to jedno z najczΩstszych, o ile nie najczΩstsze ich zastosowanie! I to do tego stopnia, ┐e wiele takich miejsc nie jest domy╢lnie typu Integer, ale w│a╢nie Pointer lub TObject.

Nie bez powodu tak d│ugo zwlekam z napisaniem, jak nazywa siΩ typ wska╝nikowy. Ot≤┐ sprawa nie jest taka prosta, jak w przypadku innych typ≤w. Typem wska╝nikowym uniwersalnym jest typ Pointer. Nie ma on zdefiniowanego typu danych, kt≤re znajduj▒ siΩ pod wskazywanym przez niego adresem. Mo┐na by rzec, ┐e jest bezpostaciowy, amorficzny. Jego cech▒ jest kompatybilno╢µ ze wszystkimi innymi typami wska╝nikowymi. Nie posiada on natomiast pewnej funkcjonalno╢ci „zwyk│ych” wska╝nik≤w. Istnieje wiele predefiniowanych typ≤w wska╝nikowych. Ich nazwy zaczynaj▒ siΩ od litery P. Aby zdefiniowaµ w│asny taki typ, nale┐y pos│u┐yµ siΩ znakiem ^:

type
PInteger = ^Integer;
TMojRec = record
Pole1: TDateTime;
Pole2: Integer;
end;
PMojRec = ^TMojRec;

A oto, jak mo┐na przyk│adowo u┐ywaµ liczby 32-bitowej w roli wska╝nika:

var
Rec: PMojRec;
begin
New(Rec);
Liczba_Integer := Integer(Rec);

Wska╝nik do obiektu

Pisz▒c o wska╝nikach celowo zaprezentowa│em wska╝niki do typ≤w prostych oraz do rekord≤w. Pomin▒│em natomiast zagadnienie u┐ywania wska╝nik≤w w programowaniu zorientowanym obiektowo (ang. OOP – Object Oriented Programming). Tutaj nie definiuje siΩ wska╝nik≤w jako takich. Delphi, a precyzyjniej Object Pascal ma to do siebie, ┐e, w przeciwie±stwie do C++, nie mo┐na w tym jΩzyku tak po prostu zdefiniowaµ zmiennej typu obiektowego (klasy). W C++, aby stworzyµ wska╝nik do instancji (egzemplarza) obiektu danej klasy, nale┐y jawnie zadeklarowaµ go jako wska╝nik. Inaczej jest w Delphi – tu ka┐da zmienna, ka┐de pole typu TObject lub jego potomka to automatycznie wska╝nik – nie da siΩ tego obej╢µ. Tak wiΩc aby stworzyµ faktyczny obiekt w pamiΩci nie wystarczy zadeklarowaµ zmiennej odpowiedniego typu. Trzeba tak┐e ten obiekt utworzyµ za pomoc▒ jednego z jego konstruktor≤w. Konstruktor to tak naprawdΩ nic innego, jak funkcja, kt≤ra zwraca wska╝nik do nowo utworzonej instancji obiektu. A wiΩc:

var
Button: TButton;
begin
Button := TButton.Create(...);

Button to w rzeczywisto╢ci wska╝nik, zmienna wska╝nikowa. Na pocz▒tku jej warto╢µ jest nieustalona. Po instrukcji przypisania przechowuje ona wska╝nik do instancji obiektu w pamiΩci. A jak wska╝nik – to wiadomo co. 32 bity itd.! No wiΩc:

Liczba_Integer := Integer(Przycisk);

Dzia│a? Musi dzia│aµ! I tak oto mamy kolejne (jedno z najwa┐niejszych) zastosowanie liczby 32-bitowej.

Kolor

Do przechowywania koloru s│u┐y typ TColor. Zdefiniowany jest on w module Graphics w nastΩpuj▒cy spos≤b:

type
TColor = -$7FFFFFFF-1..$7FFFFFFF;

Jest to wiΩc typ bΩd▒cy podzbiorem liczb typu Integer. Aby zmie╢ciµ ca│y jego zakres, potrzebne s▒ 32 bity! Nie s▒ co prawda wszystkie wykorzystane, ale co z tego? Typ TColor u┐ywany jest w wielu miejscach Delphi. A skoro zajmuje 32 bity, mamy nastΩpne zagadnienie, kt≤re kwalifikuje siΩ do wykorzystania element≤w maj▒cych za cel przechowywanie 32-bitowych liczb. Je╢li taka liczba zdefiniowana jest jako typu Integer, nie potrzeba ┐adnego rzutowania typ≤w ani konwersji. Mo┐na wszystko za│atwiµ za pomoc▒ zwyk│ego przypisania.

Liczba_Integer := Button.Font.Color;

Bajty i bity

Dotychczas u┐ywali╢my liczby 32-bitowej traktuj▒c j▒ jako ca│o╢µ. Wynika│o to z jednej z uwag wspomnianych we wstΩpie – liczby 1 i 2-bajtowe i tak przechowywane s▒ w 4 bajtach. Ale co, je╢li do dyspozycji mamy wiele liczb 32-bitowych? Powiedzmy: po jednej dla ka┐dego elementu, z kt≤rym skojarzone dane musimy gdzie╢ przechowywaµ. Czy nie op│aca│oby siΩ wykorzystaµ z tych 32 bit≤w osobno pojedynczych s│≤w (po 2 bajty), bajt≤w, a nawet bit≤w? Okazuje siΩ, ┐e tak. Powiem wiΩcej: sam Windows API stosuje tak▒ technikΩ. Za przyk│ad niech pos│u┐y jedna z najprostszych funkcji: MessageBox. Jej nag│≤wek, cytowany za Win32 Programmer’s Reference brzmi nastΩpuj▒co:

int MessageBox(
HWND hWnd,	// handle of owner window
LPCTSTR lpText,	// address of text in message box
LPCTSTR lpCaption,	// address of title of message boxUINT uType 	// style of message box
 );

Ostatni parametr jest typu UINT, kt≤rego odpowiednikiem w Delphi jest m.in. LongWord. Podaje siΩ w nim sumΩ warto╢ci, za kt≤re u┐ywa siΩ sta│ych rozpoczynaj▒cych siΩ od MB_. W rzeczywisto╢ci oznaczaj▒ one poszczeg≤lne bity 32-bitowej liczby. Bo gdyby tw≤rcy Windows chcieli ka┐d▒ z takich flag odbieraµ w osobnym parametrze tej funkcji typu BOOL (Boolean), to popatrz, jak wielkie by│oby to marnotrawstwo! Za ka┐dym parametrem u┐ywane 32 bity, wykorzystywany 1! A tak: mamy jedn▒ liczbΩ 32-bitow▒. W parametrze sumujemy sobie sta│e. Funkcja sprawdza, kt≤re z bit≤w tego parametru s▒ ustawione i na tej podstawie pokazuje odpowiednio wygl▒daj▒ce i zachowuj▒ce siΩ okienko z komunikatem. Tak wiΩc u┐ywanie 32-bitowych liczb w roli flag ma g│Ωboki sens. Teraz zastan≤wmy siΩ, jak mo┐na to zrealizowaµ. Co prawda operacje na bitach to obszerny temat (byµ mo┐e po╢wiΩcΩ mu osobny artyku│), ale podstawowe sprawy postaram siΩ poni┐ej przedstawiµ. Niech wiΩc Flaga bΩdzie zmienn▒ typu Cardinal.

var
Flaga: Cardinal;

Zdefiniujemy sobie sta│e:

const
mf_Flag1 = 1;
mf_Flag2 = 2;
mf_Flag3 = 4;
mf_Flag4 = 8;

itd... Teraz przypisujemy zmiennej warto╢µ tak▒, ┐eby ustawione by│y bity 2 i 4.

Flaga := mf_Flag2 + mf_Flag4;

lub

Flaga := mf_Flag2 or mf_Flag4;

W pierwszym przypadku wykonujemy arytmetyczn▒ operacjΩ dodawania liczb. W drugim bitow▒ operacjΩ OR. Efekt jest jednak ten sam. Zmienna zawiera teraz liczbΩ 10. Ale to jest ma│o wa┐ne. Jej znaczenie staje siΩ bardziej jasne, je┐eli jej warto╢µ przedstawimy w systemie binarnym. BΩdzie to 00001010. Czyli widzimy, ┐e ustawione s▒ bity 2 i 4. I o to chodzi│o!

Teraz sprawd╝my, kt≤re bity s▒ ustawione. Jak to siΩ robi? Trzeba sobie „przefiltrowaµ” zmienn▒ z kt≤r▒╢ ze sta│ych operacj▒ AND. Wtedy wynikiem jest albo warto╢µ tej sta│ej (bit by│ ustawiony), albo 0 (nie by│).

if (Flaga and mf_Flag2) = mf_Flag2 then
{ Co siΩ ma staµ, je╢li bit jest ustawiony };

»eby rozk│adaµ sobie liczbΩ 32-bitow▒ na poszczeg≤lne s│owa, a p≤╝niej bajty u┐yj wbudowanych funkcji High i Low. Przydatne mog▒ siΩ te┐ okazaµ operatory shr i shl. Pozosta│e rzeczy uda Ci siΩ zrobiµ za pomoc▒ ca│kowitych operacji arytmetycznych, jak + lub * oraz logicznych, jak AND, OR czy XOR.

Gdzie s▒ liczby 32-bitowe?

Du┐o dotychczas m≤wili╢my o tym, co mo┐na przechowywaµ w „miejscach” na dane zajmuj▒ce 32 bity. Teraz zastan≤wmy siΩ, gdzie takie miejsca mo┐na znale╝µ. Wbrew pozorom takie miejsca s▒ i wcale nie trzeba ich deklarowaµ we w│asnym zakresie. Delphi zawiera wiele mechanizm≤w celowo przewidzianych dla przechowywania tego typu danych. Poni┐ej omawiamy niekt≤re z nich.

W│asne zmienne

Najbardziej oczywistym miejscem na przechowywanie danych 32-bitowych jest oczywi╢cie zmienna. Tak▒ zmienn▒ mo┐esz sobie sam zadeklarowaµ. Czy to bΩdzie zmienna globalna w programie, w module, w sekcji interface czy implementation, czy te┐ zmienna lokalna procedury, funkcji lub metody. W zmiennej takiej przechowuje siΩ zwykle jedn▒ konkretn▒ rzecz. Dlatego nie jest potrzebne zwykle rzutowanie typ≤w. Po prostu deklarujesz zmienn▒ takiego typu, jakiego typu dane bΩdziesz w niej przechowywa│. Mo┐e to byµ np. typ Integer lub Longint, Cardinal lub LongWord, Pointer czy dowolny inny wska╝nik, TObject lub dowolna inne klasa, TColor albo jakikolwiek inny typ, z kt≤rego wyra┐enie SizeOf(Nazwa_typu) zwraca liczbΩ 4 (4 bajty = 32 bity).

W│a╢ciwo╢µ TComponent.Tag

Byµ mo┐e zauwa┐y│e╢, ┐e wszystkie komponenty, jakie stawiasz na formatce posiadaj▒ w│a╢ciwo╢µ Tag. Jest ona typu Longint, a wiΩc jest to 32-bitowa liczba ca│kowita za znakiem. Je╢li interesowa│e╢ siΩ tym bli┐ej, wiesz mo┐e, ┐e nie jest i nie bΩdzie ona u┐ywana przez Delphi. Jest po prostu 32-bitowe pole do wykorzystania dla programisty w dowolny spos≤b.

Do czego takie pole w komponencie mog│oby siΩ przydaµ? Przyznam Ci siΩ szczerze, ┐e sam u┐y│em go tylko 2 razy w ┐yciu. W obydwu przypadkach chodzi│o o Timer. Po prostu ka┐dy cykl Timera mia│ inkrementowaµ pewien licznik. Licznik ten mog│em oczywi╢cie zdefiniowaµ jako zmienn▒. Logiczne wyda│o mi siΩ jednak u┐ycie w tej roli w│a╢ciwo╢ci Tag Timera, jako, ┐e ten licznik niejako do niego nale┐a│, by│ z nim w jaki╢ spos≤b zwi▒zany.

W│a╢ciwo╢µ Tag nie jest jednak tak bezu┐yteczna, jak mog│oby siΩ wydawaµ. Wyobra╝ sobie: piszesz program, w kt≤rym u┐ytkownik mo┐e dowolnie konfigurowaµ, jakie przyciski maj▒ siΩ znale╝µ na pasku narzΩdzi. Z tego wniosek – przyciski te musz▒ byµ tworzone dynamicznie w czasie dzia│ania programu. I tutaj pojawia siΩ kilka problem≤w. Zdarzeniu OnClick ka┐dego z tych przycisk≤w bΩdzie podczas jego tworzenia przypisywany adres procedury, kt≤r▒ wcze╢niej zdefiniowa│e╢ w kodzie. Sk▒d ona ma wiedzieµ, z kt≤rego przycisku pochodzi zdarzenie? Rozwi▒zaniem jest tutaj w│a╢nie w│a╢ciwo╢µ Tag. Dla przyk│adu: bΩdziesz przechowywa│ w jakie╢ tablicy czy innej strukturze danych informacje o ka┐dym z tych przycisk≤w. Podczas dynamicznego ich tworzenia w│a╢ciwo╢ci Tag ka┐dego z przycisk≤w przypiszesz numer (indeks) rekordu, kt≤ry opisuje ten przycisk b▒d╝ bezpo╢rednio wska╝nik do takiego rekordu. Procedura obs│ugi zdarzenia OnClick zawsze otrzymuje w parametrze Sender wska╝nik do obiektu, na rzecz kt≤rego zosta│a wywo│ana. Typ tego wska╝nika to TObject. Je┐eli jednak masz pewno╢µ, ┐e procedura obs│uguje wy│▒cznie zdarzenia przycisk≤w TToolButton, mo┐esz bez sprawdzania, czy

(Sender is TToolButton)

za│o┐yµ, ┐e tak jest w i zawsze traktowaµ Sender jako wska╝nik na instancjΩ obiektu klasy TToolButton. Sprawdzasz wiΩc warto╢µ Tag takiego przycisku i ju┐ wiesz, o kt≤ry przycisk chodzi, a co za tym idzie wiesz, kt≤re polecenie trzeba wywo│aµ w reakcji na jego klikniΩcie.

procedure Button1Click(Sender: TObject);
begin
MojaZmienna := (Sender as TToolButton).Tag;
if MojaZmienna = ... then ...
end;

W│a╢ciwo╢µ Data w TListItem i TTreeNode

Czym s▒ windowsowe kontrolki ListView i TreeView, nie trudno siΩ dowiedzieµ. Wystarczy w│▒czyµ Eksplorator Windows. Drzewo folder≤w po lewej stronie to TreeView, czyli widok drzewa. Lista folder≤w i plik≤w po prawej stronie to ListView, czyli widok listy. Ten ostatni mo┐e byµ w jednym z 4 stan≤w: du┐e ikony, ma│e ikony, lista lub szczeg≤│y. Element≤w ka┐dego z tych widok≤w nie da siΩ przechowywaµ w zwyk│ych stringlistach, jak to robi m.in. ListBox. Tutaj ka┐dy element opisywany jest przez poka╝ny zesp≤│ cech, jakie posiada. Dlatego stworzono specjalne klasy reprezentuj▒ce te elementy. S▒ to odpowiednio TlistItem dla elementu widoku listy i TtreeNode dla elementu widoku drzewa. ú▒czy je jedna cecha: obydwie posiadaj▒ w│a╢ciwo╢µ Data typu Pointer. Jest to miejsce na 32-bitow▒ dan▒ do dowolnego wykorzystania dla programisty. Jednym s│owem: je╢li chcesz razem z ka┐dym elementem listy lub drzewa przechowywaµ jak▒╢ dan▒, kt≤ra mie╢ci siΩ w 32 bitach, zapisz j▒, po wykonaniu koniecznego rzutowania typ≤w, w Data. Je╢li dane te nie mieszcz▒ siΩ w 32 bitach, stw≤rz w│asny typ rekordowy oraz wska╝nikowy do niego lub w│asn▒ klasΩ. Potem bΩdziesz dla ka┐dego elementu alokowa│ dynamicznie w pamiΩci nowy egzemplarz tego typu, a w pole Data wpisywa│ wska╝nik do niego.

W│a╢ciwo╢µ Objects w TStrings

KlasΩ TStrings i jej pochodn▒ – TStringList zna chyba ka┐dy, choµ mo┐e nie ka┐dy zdaje sobie z tego sprawΩ. To w│a╢nie w tej klasie, przeznaczonej do przechowywania wielolinijkowego tekstu, zapisywana jest zawarto╢µ komponent≤w Memo, ListBox, CheckListBox i wielu innych. Tak wiΩc Memo1.Lines, ListBox1.Items, CheckListBox1.Items, ListView1.Items[x].SubStrings to w│a╢nie pola typu TStrings. To z nich mo┐esz odczytywaµ i do nich zapisywaµ ca│y tekst, poszczeg≤lne linijki lub nawet pojedyncze znaki. Klasa TStringList, bΩd▒ca potomkiem TStrings, przeznaczona jest do wykorzystania przez programistΩ we w│asnym zakresie – programista musi sam zadeklarowaµ i utworzyµ obiekt tej klasy, a informacje tekstowe w nim przechowywane nie s▒ nigdzie prezentowane w spos≤b automatyczny – programista mo┐e je wykorzystaµ w dowolny spos≤b.

var
SL: TStringList;
begin
SL := TStringList.Create;
SL.Add('1 linijka');

Nie ka┐dy jednak wie, ┐e z ka┐d▒ z linijek tekstu skojarzona jest... 32-bitowa liczba! Tak jest! Przy ka┐dej linijce tekstu mo┐esz przechowywaµ sobie dowoln▒ 32-bitow▒ informacjΩ – np. o kolorze, jaki ma mieµ dana linijka tekstu, jak▒╢ liczbΩ czy te┐ wska╝nik. Liczby te s▒ typu TObject, ale mo┐na je wykorzystaµ w dowolny spos≤b. Aby dodaµ now▒ linijkΩ od razu przypisuj▒c skojarzon▒ z ni▒ liczbΩ, zamiast Add u┐yj metody AddObject:

SL.AddObject('1 linijka', TObject(x));

DostΩp do tych liczb masz w obie strony (zapis / odczyt) za pomoc▒ tablicowej w│a╢ciwo╢ci Objects. Je╢li chcesz teraz zmieniµ warto╢µ liczby skojarzonej z 1 linijk▒ tekstu, u┐yj instrukcji:

SL.Objects[0] := TObject(y);

Jakie jest praktyczne wykorzystanie klasy TStringList? Okazuje siΩ, ┐e przeogromne! Je╢li masz przechowywaµ listΩ rekord≤w, a ka┐dy z tych rekord≤w zawiera jedno pole typu │a±cuchowego – StringList jest jak znalaz│! Deklarujesz w│asny rekord i wska╝nik do niego lub klasΩ, kt≤ra bΩdzie przechowywa│a wszystkie pozosta│e pola jednego rekordu. Wszystkie opr≤cz wspomnianego │a±cucha. NastΩpnie dla ka┐dego nowo tworzonego rekordu: tworzysz dynamicznie w pamiΩci nowy egzemplarz danego typu rekordowego lub klasy, wype│niasz go danymi, do StringListy dodajesz │a±cuch skojarzonej z ni▒ liczbie przypisuj▒c jednocze╢nie skojarzonej z ni▒ liczbie wska╝nik do nowo zaalokowanej pamiΩci.

Wykorzystanie StringList≤w mo┐na te┐ stosowaµ w bardziej zaawansowany spos≤b i w po│▒czeniu np. z innymi stringlistami lub np. z typem TList. Przyk│ad: masz stworzyµ strukturΩ danych, kt≤rej rekord sk│ada siΩ z dw≤ch │a±cuch≤w. Nic prostszego! Deklarujesz 2 StringListy. Dla ka┐dego tworzonego rekordu dodajesz do ka┐dego StringLista odpowiedni │a±cuch. Usuwaj▒c za╢ – usuwasz │a±cuch o danej pozycji r≤wnocze╢nie z obu list. W ten oto spos≤b obydwie listy przechowuj▒ zawsze tΩ sam▒ liczbΩ element≤w, a ich pozycjΩ o tych samych indeksach przechowuj▒ po jednym polu z jednego rekordu.

Klasa TList

Jest ma│o znana. Na czym polega jej dzia│anie? Na pocz▒tek mo┐e napiszΩ, ┐e jest do wykorzystania dla programisty – trzeba j▒ sobie zadeklarowaµ i stworzyµ we w│asnym zakresie podobnie, jak TStringList.

var
L: TList;
begin
L := TList.Create;

TList jak sama nazwa wskazuje, to taki TStringList bez string≤w. A je╢li z klasy przeznaczonej do przechowywania listy │a±cuch≤w odejmiemy │a±cuchy, to co nam zostanie? Nic? Nie! PamiΩtasz? StringList potrafi przechowywaµ skojarzon▒ z ka┐dym │a±cuchem liczbΩ 32-bitow▒. Czyli? Klasa TList to klasa przeznaczona do przechowywania listy liczb 32-bitowych!

Jak to wykorzystaµ? Na przyk│ad: je╢li masz przechowywaµ listΩ jaki╢ liczb, indeks≤w, kolor≤w itp. A w praktyce, wykorzystuj▒c te liczby w roli wska╝nik≤w, mo┐esz pomagaj▒c sobie klas▒ TList tworzyµ dowolne listowe struktury danych. Po prostu: deklarujesz typ rekordowy lub klasΩ. Tworzysz w pamiΩci egzemplarze tej klasy czy tego rekordu, a na li╢cie przechowujesz wska╝niki do niego. Prawda, ┐e przydatne? A jakie uniwersalne! Tylko nie zapomnij przed usuniΩciem pozycji z listy zwolniµ pamiΩµ zajmowan▒ przez strukturΩ, do kt≤rej ta pozycja przechowuje wska╝nik!

Bezpiecze±stwo

Podczas przeprowadzania opisywanych wy┐ej operacji mo┐e wyst▒piµ wiele b│Ωd≤w i sytuacji, kt≤re z pewno╢ci▒ nie s▒ po┐▒dane przez programistΩ. W rozdziale tym poruszΩ tak┐e temat warto╢ci „pustych”, ich znaczenie, realizacjΩ w praktyce oraz bezpieczne wykorzystanie.

Warto╢ci „puste”

Przechowuj▒c w miejscu na 32-bitow▒ liczbΩ dane jakiego╢ rodzaju czΩsto przychodzi ustaliµ pewn▒ warto╢µ, kt≤r▒ bΩdzie oznacza│a warto╢µ „pust▒”. W przypadku liczby mo┐e to byµ np. 0. W przypadku przechowywania indeksu, gdzie elementy liczone s▒ od 0, za warto╢µ tak▒ przyjmuje siΩ zwykle –1. Dla wska╝nik≤w jest to nil.

Bezpiecze±stwo warto╢ci pustych

U┐ywaj▒c warto╢ci pustych (ich u┐ycie nie zawsze jest konieczne) poci▒ga za sob▒ konieczno╢µ „pilnowania” – czΩstego sprawdzania przed u┐yciem, czy dana warto╢µ nie jest pusta. Je╢li np. chcesz zaindeksowaµ StringList indeksem odczytanym z takiego miejsca, a zapisana tam warto╢µ jest „pusta” – wynosi –1 – bΩdzie b│▒d „List index out of bounds (-1)”. Dlatego zawsze sprawdzaj przed u┐yciem, czy warto╢µ nie jest pusta, je╢li nie masz co do tego pewno╢ci.

Podobnie ze wska╝nikami – zawsze sprawdzaj, czy nie wynosz▒ one nil. W przeciwnym przypadku bΩdzie „Access Violation”, a tego typu b│Ωdy s▒ wyj▒tkowo wredne i trudne do zlokalizowania. Dlatego trzeba, szczeg≤lnie przy operacjach na wska╝nikach, zapobiegaµ im najlepiej, jak tylko siΩ da.

Bezpiecze±stwo wska╝nik≤w

Jak ju┐ wy┐ej wspomnia│em, wska╝niki to bardzo b│Ωdogenna dziedzina programowania. Dlatego podczas operowania na nich trzeba wyj▒tkowo uwa┐aµ. Oto kilka wskaz≤wek dotycz▒cych wska╝nik≤w:

  1. Zawsze przed odwo│aniem siΩ do miejsca w pamiΩci, na kt≤re wska╝nik wskazuje sprawdzaj, czy jego warto╢µ nie wynosi nil.
  2. Je╢li zwalniasz pamiΩµ, wszystkim wska╝nikom, kt≤re na ten obszar wskazywa│y przypisz warto╢µ nil. W przeciwnym wypadku bΩd▒ one skierowane na przypadkowy blok pamiΩci.
  3. Usuwaj▒c element przechowuj▒cy wska╝nik do jakiej╢ struktury w pamiΩci nie zapomnij uprzednio zwolniµ tej pamiΩci.

Oczywi╢cie nie zawsze trzeba stosowaµ wszystkie wymienione tutaj zalecenia. Zale┐y to od konkretnego przypadku.

Og≤lnie nieprawid│owo╢ci w operacjach na wska╝nikach mo┐na podzieliµ na dwie kategorie:

  1. Zwolnienie pamiΩci tam, gdzie nie trzeba i wtedy, kiedy nie potrzeba. Potem program bΩdzie siΩ pr≤bowa│ odwo│aµ do ju┐ nieistniej▒cej struktury i powstanie b│▒d.
  2. Niezwolnienie pamiΩci tam, gdzie jest to konieczne. PamiΩµ pozostanie zaalokowana, a ty, usuwaj▒c wszystkie wska╝niki do niej, stracisz mo┐liwo╢µ jakiegokolwiek z ni▒ kontaktu. PamiΩµ siΩ zmarnuje.

Bezpiecze±stwo list

Je╢li przechowujesz jakie╢ elementy na li╢cie np. TStrings, TStringList czy TList, pamiΩtaj o kilku rzeczach.

Zawsze uwzglΩdnij, co bΩdzie, je╢li lista jest pusta tzn. zawiera 0 element≤w. Nie musisz tego robiµ jedynie, je╢li stosujesz pΩtlΩ for:

for I := 0 to SL.Count-1 do ...

Je╢li lista jest pusta, SL.Count = 0. Z tego wniosek, ┐e SL.Count-1 wynosi –1, co jest mniejsze od warto╢ci pocz▒tkowej 0. W takim przypadku pΩtla nie wykona siΩ ani raz.

Je╢li masz 2 lub wiΩcej list przechowuj▒cych pola wsp≤lnej struktury danych, zawsze pamiΩtaj, aby usuwaµ i dodawaµ elementy do wszystkich na raz. DziΩki temu wszystkie bΩd▒ zawsze przechowywa│y tak▒ sam▒ ilo╢µ element≤w, a elementy o konkretnym indeksie ka┐dej z list bΩd▒ zawiera│y pola jednego rekordu.

Je╢li integrujesz po wszystkich elementach listy usuwaj▒c przy okazji niekt≤re z nich, zawsze r≤b to od ko±ca. Je╢li bowiem napiszesz tak:

{ ¼LE!!! }
for I := 0 to SL.Count-1 do
if SL[I] = x then SL.Delete(I);

Zastan≤w siΩ, co bΩdzie w nastΩpuj▒cej sytuacji: Lista liczy sobie 3 elementy o indeksach, wiadomo, 0, 1 i 2. PΩtla ma siΩ wiΩc wykonaµ w 3 krokach, w kt≤rych zmienna steruj▒ca I przybiera│a bΩdzie kolejno takie w│a╢nie warto╢ci. Oto dochodzi do pierwszego elementu – indeks 0. Program zostawia go w spokoju. Idzie do drugiego. Ten trzeba usun▒µ. Robi wiΩc to. Od tej chwili lista liczy ju┐ tylko 2 elementy: pierwszy 0 i drugi 1, przesuniΩty w d≤│ po usuniΩciu ╢rodkowego. PΩtla natomiast bΩdzie chcia│a uzyskaµ dostΩp do elementu 3 (indeks 2) i... „List index out of bounds (2)”! Dlatego prawid│owo bΩdzie:

{ DOBRZE }
for I := SL.Count-1 downto 0 do
if SL[I] = x then SL.Delete(I);

Zako±czenie

ZdajΩ sobie sprawΩ, ┐e opisany tu przeze mnie temat nie jest jaki╢ wyj▒tkowo trudny ani wa┐ny. Ma jednak jedn▒ szczeg≤ln▒ cechΩ. Mianowicie takiego zestawienia nie spotkasz w ┐adnej ksi▒┐ce czy innej dokumentacji. Ja przynajmniej do tej pory nie spotka│em. Zawarte tu informacje w ma│ej czΩ╢ci opar│em na jaki╢ konkretnych ╝r≤d│ach, za to w decyduj▒cej wiΩkszo╢ci na w│asnych do╢wiadczeniach w 4-letniej praktyce programistycznej. Dlatego prezentowany poni┐ej spis literatury i dokumentacji nale┐y traktowaµ mniej jako dokumenty, z kt≤rych korzysta│em podczas pisania tego artyku│u, a bardziej jako dokumenty, kt≤rych przeczytanie lub z kt≤rych korzystanie polecam Tobie, drogi czytelniku. Mam nadziejΩ, ┐e zaprezentowane tu przeze mnie informacje pomog▒ wielu „tw≤rcom program≤w”, kt≤rzy potrafi▒ tylko postawiµ komponenty na formatce, staµ siΩ prawdziwymi programistami.

Literatura:

  1. MSDN Library – January 2000.
  2. Stephens Rod: Algorytmy i struktury danych z przyk│adami w Delphi. Helion, 2000.
  3. Win32 Programmer's Reference.
  4. Wr≤blewski Piotr: Algorytmy, struktury danych i techniki programowania. Helion, 1997.

Adam Sawicki
sawickiap@poczta.onet.pl