Wskaźniki w Pascalu to temat który wypada dobrze zrozumieć, aby być bardziej kompletnym programistą. Dlatego napisałem poniższe podstawy, gdy już sam je poznałem.
Podstawowe wiadomości
Wskaźniki jest to pewien typ zmiennych obok liczbowych(integer) i łańcuchowych(string). Typ wskaźnikowy w postaci binarnej jest identyczny z typem liczbowym całkowitym i zawiera czterobajtową liczbę, która jest adresem komórki w pamięci komputera.
Deklaracja takiego typu wygląda następująco:
Type _TypWskaznikowy = ^TypWskazywany
co możemy odczytać następująco: od tej chwili każda zmienna typu _TypWskaznikowy jest czterobajtowym adresem, pod którym można znaleźć wartość typu _TypWskazywany. Daszek jest tym specjalnym symbolem, który nadaje takie znaczenie tej deklaracji. Konkretniejszy przykład:
Type _WskWord = ^Word
czyli każda zmienna typu _WskWord będzie wskazywać(zawierać liczbowy adres w pamięci) na wartość typu Word.
Zadeklarujmy teraz zmienną typu wskaźnikowego:
Type _WskWord = ^Word Var Wsk:_WskWord begin
Słowem begin rozpoczęliśmy program bądź procedurę. Zmienna Wsk ma na początku wartość przypadkową. Zawiera przypadkowy adres, pod którym możemy znaleźć liczbę dwubajtową (składają sie na nią dwie kolejne komórki pamięci: adres,adres+1). Dlatego dobrze zawsze na początku podstawić pod zmienną wartość nil. Jest to specjalny adres pusty (niekoniecznie zerowy), oznacza że zmienna nie wskazuje w tej chwili na żaden konkretny adres.
Wsk:=nil;
Dzięki temu później będziemy mogli się dowiedzieć czy zmienna wskazuje na coś nam potrzebnego poprzez konstrukcję warunkową:
if Wsk=nil then else
Wskaźniki w użyciu
Spróbujemy teraz przechować coś w zmiennych typu wskaźnikowego. Program nasz wygląda teraz tak:
Type _WskWord = ^Word Var Wsk:_WskWord begin Wsk:=nil;
Dopisujemy instrukcję:
New(Wsk)
W trakcie wykonania tej ostatniej instrukcji program znajdzie wolne miejsce(adres) w pamięci na przechowanie dwubajtowej liczby oraz adres ten stanie się wartością zmiennej Wsk. Przypominam, że zmienna Wsk wskazywała od początku na adres przypadkowy, ale wcale nie musiałby on być wolny i zapisanie tego obszaru nową liczbą typu Word (to za chwilę) mogłoby się skończyć źle dla systemu. Następnie wpisaliśmy tam nil (w naszym przypadku niepotrzebnie, ale dobrze zawsze tak postępować po zadeklarowaniu nowej zmiennej).
Teraz mamy już miejsce. Wpiszmy jakąś wartość
Wsk^:=45;
Drugi raz stosujemy daszek, tym razem w innym celu. Oznacza on, że nie zmieniamy zmiennej Wsk (która jest adresem), ale obszar pamięci przez nią wskazywany. Możemy odczytywać wartość (Zmienna a jest typu Word):
a:=Wsk^;
Gdy wskaźnik nie jest już potrzebny
Niestety wymaga przy tym naszej pomocy. Wydając polecenie:
Dispose(Wsk);
spowodujemy że obszar pamięci zajęty wcześniej po wykonaniu instrukcji New zostanie ponownie zaznaczony jako wolny do ponownego wykorzystania.
Więcej wskaźników w użyciu
Korzystając z powyższej wiedzy możemy trochę poeksperymentować, zadeklarujmy
Type _WskWord = ^Word Var Wsk1,Wsk2:_WskWord begin Wsk1:=nil; Wsk2:=nil; New(Wsk1); New(Wsk2); Wsk1^:=10; Wsk2^:=20;
Teraz prześledzimy dwa warianty:
Wsk1^:=Wsk2^
Dziesiątka w obszarze wskazywanym przez Wsk1 zostanie zastąpiona 20. (Wsk1^ równa się 20 oraz Wsk^ równa się 20), ale to nadal są dwa różne miejsca w pamięci i zmiana jednego nie będzie miała wpływu na drugi.
Wsk1:=Wsk2
Takiego zapisu jeszcze nie było oznacza, że adres(bo zmienna bez daszka) w zmiennej Wsk1 będzie taki sam jak w zmiennej Wsk2, czyli obie zmienne wskazują ten sam obszar pamięci (Wsk1^ równa się 20 oraz Wsk^ równa się 20), ale zapis Wsk1^:=7 spowoduje, że również Wsk2^ będzie się równało 7 !!!. Przy okazji trzeba zauważyć że po tym podstawieniu zgubiliśmy gdzieś jeden obszar w pamięci (zawiera 10, wskazywany wcześniej przez Wsk1, zostanie on niepotrzebnie zajęty aż do skończenia programu). oto schemat naszego ostatniego wariantu:
Wsk1 --|-\ \-> 10 | | --------> Wsk2 ----------> 20
Widać więc, że zmienne wskaźnikowe niosą pewne zagrożenia i wymagają nieco uwagi, ale nadal nie widać po co one są nam potrzebne. Wszystkie te powyższe operacje można wygodniej zrobić na normalnych zmiennych typu word.
Lista jednokierunkowa
Teraz przekonamy się o zaletach wskaźników. Przypuśćmy, że chcielibyśmy w tablicy zgromadzić dane o jakichś elementach, tyle że nie wiemy ile ich może być np. tablica z klientami firmy. Deklarujemy więc wtedy tablicę o rozmiarze, który uważamy za maksymalną liczbę elementów oraz zmienną pokazującą jaka część tablicy jest wypełniona. Widać, że w skrajnych przypadkach (jeden element zajęty w dużej tablicy) następuje niepotrzebnie znaczna utrata pamięci.
Rozwiążemy to zadanie przy pomocy wskaźników ekonomiczniej (na ogół). Zastosujemy nową konstrukcję listy. Zadeklarujemy:
Type _WskElem = ^_Element; _Element = record Wartosc:{typ danych przechowywanych w elemencie listy}; NastElem:_WskElem end; Var Lista:_WskElem;
Czas na wyjaśnienie, bo pojawiło się dużo nowych rzeczy. Od góry: typ _WskElem wskazuje na typ _Element (Uwaga: to jest wyjątek w deklararowaniu w Pascalu, że pojawia się zmienna _Element, która jeszcze nie jest zadeklarowana. Możemy tak postąpić jedynie dla zmiennych typów wskaźnikowych). Następnie typ _Element będący rekordem, składa się z pola Wartosc, gdzie będziemy przechowywać zawartość komórki (jest odpowiednikiem typu składowego w tablicy), oraz pola NastElem, które jest wskażnikiem na następny element listy.
Nasza lista będzie zmienną typu _WskElem – wskaźnik na pierwszy element listy. Poniżej narysowany jest schemat naszej listy w działaniu:
|Wartosc1 | |Wartosc2 | Lista ---->| | --->| | --->nil |NastElem-|---| |NastElem-|---|
W naszym przypadku zawiera ona dwa elemnty, tak jak gdyby była to tablica array[1..2]of TypWartosc. Ostatni element listy nie wskazuje na żaden element, powinno być tam zatem nil.
Operacje na liście jednokierunkowej
Teraz spróbujemy taką listę stworzyć. Na początek zgodnie z zasadami
Lista:=nil
Teraz dodamy pierwszy element:
New(Lista); Lista^.Wartosc:={to co chcemy wstawić}; Lista^.NastElem:=nil
i sytuacja wygląda w tej chwili tak:
|Wartosc1 | Lista ---->| | --->nil |NastElem-|---|
Jeśli teraz chcielibyśmy dodać jakiś element na koniec to musielibyśmy przejść całą listę znajdując kolejne wskaźniki, do chwili aż ten wskaźnik będzie równy nil, wtedy zamiast niego wstawimy nowy element. Znacznie łatwiej wstawić na początek:
Var WskRob:_WskElem; begin New(WskRob); WskRob^.Wartosc:={jakaś wartość}; WskRob^.NastElem:=Lista; Lista:=WskRob;
W dwóch ostatnich linijkach dokonujemy operacji, aby wskaźniki naszego nowego elementy wskazywał na pierwszy element starj listy, a wskaźnik Lista (do początku listy) na nasz nowy element. Podobne to jest do wciśnięcia się nowego elemntu na początek listy. Należy zauważyć, że w pamięci te elementy mogą być rozłożone dowolnie i w dodatku w rozproszeniu i raczej nie w kolejności listy (w odróżnieniu od tablic, w których wszystkie elementy zajmują spójny kawałek pamięci). W celu ułatwienia operacji dodawania na koniec listy można trzymać wskaźniki do początku i końca listy:
Type _Lista = record czolo,ogon:_WskElem; end;
Niektóre operacje na wskaźnikach będą bardziej skomplikowane, ale ogólnie zyskamy na szybkości działania. cd
Ogуlna konkluzja jest zatem nastкpuj±ca. C++ to jкzyk trudniejszy i wymagaj±cy wiкkszego do¶wiadczenia, ale za to daje wiкksze moїliwo¶ci i wygodк. Delphi to jкzyk prostszy, ale za to bardziej ograniczony i mniej wygodny. Jedyny plus Delphi to jego bardzo szybki kompilator, ale wynika to z prostoty jкzyka Object Pascal.