Do czego może się przydać liczba 32 bitowa


Warsztat - Programowanie gier komputerowych :: Do czego może się przydać liczba 32-bitowa?







Strona główna Forum Szukaj Lista użytkowników
Grupy
Zarejestruj Zaloguj

Artykuły
Kliknij na kategorię żeby dodać artykuł Szukaj
Programowanie gier » Artykuły » Ogólnie o programowaniu
[Wersja do drukowania]
Do czego może się przydać liczba 32-bitowa?
Opis Co to jest liczba 32-bitowa i jak się z nią
obchodzić. Przykłady w Delphi.
Autor Adam "Regedit" Sawicki Data Sro 06 Paź, 2004 10:32
pm Typ ***
Słowa klucze
Kategoria Ogólnie o programowaniu
Odsłony 295


Do czego może się przydać liczba 32-bitowa?
Co to jest liczba 32-bitowa i jak się z nią obchodzić.
Przykłady 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ą
inna 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-biowe procesory (jak
Pentium), oraz 32-biowe 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
box
UINT 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 wieszczą 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:

Zawsze przed odwołaniem się do miejsca w pamięci, na
które wskaźnik wskazuje sprawdzaj, czy jego wartość nie
wynosi nil.
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.

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:

Zwolnienie pamięci tam, gdzie nie trzeba i wtedy, kiedy
nie potrzeba. Potem program będzie się próbował odwołać
do już nie istniejącej struktury i powstanie błąd.
Nie zwolnienie 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 interujesz 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:

MSDN Library – January 2000.
Stephens Rod: Algorytmy i struktury danych z przykładami
w Delphi. Helion, 2000.
Win32 Programmer's Reference.
Wróblewski Piotr: Algortymy, struktury danych i techniki
programowania. Helion, 1997.


Skocz do: Wybierz forumWarsztat - Programowanie
gier komputerowych|--Szkółka| |--Szkółka -
języki| |--Szkółka - grafika| |--Szkółka -
inne|--Programowanie gier| |--Ogólnie|
|--Dźwięk| |--Sztuczna Inteligencja|
|--Inne|--Programowanie grafiki|
|--Programowanie grafiki| |--OpenGL|
|--DirectX|--Produkcja| |--Pomysły|
|--Projektowanie| |--Projekty| |--Grafika|
|--Ogłoszenia|--O czym innym| |--Konferencje,
spotkania| |--Warsztat| |--Aktualności|
|--Artykuły| |--Wykłady| |--Compo|
|--Lepperlandia|--Śmietnik| |--Z odrzutu



Powered by Knowledge Base, wGEric (C) 2002 PHPBB.com MOD
This script (Knowledge Base - MX Addon v. 1.03e) is modified
by Haplo





W1.5b (C) 2004
[admin: ayufan, g[R]eK, Goliatus, mikael_, Regedit]
Wszystkie czasy w strefie CET (Europa)

Powered by phpBB2 Plus 1.52 based on phpBB 2.0.10 © 2001, 2002 phpBB
Group :: FI Theme :: Mody i Podziękowania












Wyszukiwarka

Podobne podstrony:
Amatorskie metody wykonywania płytek drukowanych, czyli do czego może służyć żelazko c d
Do czego może służyć grzebień Niecodzienny przypadek ciała obcego u psa rasy amerykański staffordsh
Czego moze sie spodziewac rolnik
Do czego przydaje się interferencja
Do czego przydaje się interferencja
znalezione gdzies na necie opracowanie (nie Pilara!) Wstęp Ogólny do Pisma Św ale moze sie komus
staniszkis bywa ze mezczyzni do czegos sie przydaja
IDOL=Moze Sie Wyd iS40
Piskulak Kluczem do baśni może wytrych
MOO programowanie liniowe(chyba się przyda!!!)
CZEGO JAŚ SIĘ NIE NAUCZY KSZTAŁCENIE DLA PRZYSZŁOŚCI
Może się wydawać Idol

więcej podobnych podstron