Programowanie w języku Delphi » Artykuły

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 &#8211; od Delphi</wiki>, przez C</wiki> na PHP</wiki> skończywszy.
Na początku zapamiętaj jedno zadanie: Wskaźnik</wiki> jest specjalną zmienną która zapamiętuje adres innej zmiennej. Zaraz dokładnie wyjaśnie o co chodzi.

Rejestry


Rejestr</wiki> to specjalny obszar pamięci wbudowany w CPU</wiki> (Central Processing Unit). Języki wysokiego poziomu posiadają wygodne funkcje &#8211; np. Writeln, które realizują pewne działanie &#8211; wyświetlanie tekstu na konsoli. W rzeczywistości jednak jedna instrukcja Writeln odpowiada instrukcjom procesora</wiki>.

Istnieje także coś takiego jak wskaźnik instrukcji, który kontroluje która instrukcja ma być wykonana jako następna. W ten sposób działa program, który jest wykonywany przez procesor. Mowię o tym dlatego, aby zapoznać Cię z paroma pojęciami.

Stos


Stos jest 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 potrzebne do prawidłowego działania programu. Z pojęciem stosu wiąże się również LIFO.

LIFO


LIFO, czyli Last In, First Out (ostatni wchodzi, a wychodzi jako pierwszy). LIFO jest algorytmem operacji na stosie; otóż stos jest uporządkowaną strukturą liniową &#8211; możesz sobie wyobrazić to na przykładzie drzewka. Podczas wykonywania jakiejś funkcji programu, na stos odkładane są pewne dane (na sam wierzch) poczym po wykonaniu operacji są od razu zdejmowane.

Często podawanym przykładem jest przykład z monetami lub z talerzami. Układając monety jedna na drugą tworzysz wieże z monet. Teraz chcąc zdjąć wszystko, zaczynasz od monety ułożonej na samej górze &#8211; identycznie działa stos &#8211; nie możesz zdjąć żadnych danych ułożonych w środku &#8211; musisz zacząć od tych na samym wierzchu.

FIFO


Innym algorytmem wykonywania działań jest FIFO - First In, Fist Out (pierwszy wchodzi, pierwszy wychodzi). Wspominamy o tym jako o ciekawostce, z którą także możesz się spotkać &#8211; nie ma to nic wspólnego ze stosem.
FIFO można porównać do kolejki sklepowej &#8211; pierwszy klient w kolejce kupuje pierwszy poczym pierwszy wychodzi. To samo można powiedzieć o drukarce &#8211; najpierw drukuje ona pierwsze strony, poczym kolejne aż w końcu drukuje ostatnią i kończy działanie.

Sterta


Powiedzieliśmy sobie, że stos to pamięć alokowana w momencie działania programu. Np. parametry funkcji oraz zmienne lokalne &#8211; są one &#8222;układane&#8221; na stosie w momencie wywołania funkcji. Po jej zakończeniu są ze stosu ściągane.
Sterta to cała dostępna 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żesz w trakcie działania programu zarezerwować sobie pewien blok pamięci &#8211; np. 1 MB</wiki>. W tym momencie możesz korzystać z tego 1 megabajta w dowolny sposób, w dowolnym miejscu w programie.

Jest tylko jedna niedogodność &#8211; w momencie, gdy dane nie będą Ci już potrzebne &#8211; Ty sam jako programista musisz zadbać o zwolnienie pamięci.

A więc obszar pamięci (Stos) nie może być w żaden sposób modyfikowany. Jeżeli piszesz mały programik to taka alokacja (statyczna) jest wystarczająca.

Tworzenie wskaźnika


Deklaracja wskaźnika następuje przy pomocy operatora ^.

var
  P : ^String;


Od tego mementu w programie możemy korzystać ze wskaźnika P, wskazującego na typ String. Wszelkie operacje dokonywana na wskaźnikach muszą korzystać z dwóch operatorów specyficznych jedynie dla typów wskaźnikowych &#8211; są to operatory ^ oraz @. Znaczenie ich poznasz w dalszej części tego rozdziału.

Przydział danych do wskaźników


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

program Pointers;
 
var
  P : ^String;
 
begin
  P^ := 'Delphi 7';
end.


Program próbuje przypisać określone dane do wskaźnika, w postaci łańcucha tekstowego; musi to się odbyć z wykorzystaniem operatora ^, w przeciwnym wypadku Delphi zasygnalizuje błąd: [Error] Pointers.dpr(12): Incompatible types: 'String' and 'Pointer'.

Spowodowane jest to tym, ze do typu wskaźnikowego nie można przypisać w normalny sposób wartości, wskaźniki muszą uprzednio wskazywać na inną, zwykła zmienną.
Przydzielaniem danych bezpośrednio do wskaźnika zajmiemy się w dalszej części tego rozdziału.
Poniższy program zostanie skompilowany, i co najważniejsze &#8211; zadziała bez problemu:

program Pointers;
 
var
  S : String;
  P : ^String;
 
begin
  S := 'Delphi'; // przypisanie danych do zwykłej zmiennej
  P := @S; // uzyskanie adresu zmiennej
  P^ := 'Borland Delphi 7 Studio'; // modyfikacja danych
end.


Uzyskiwanie adresów zmiennej


Jak widać w powyższym przykładzie po zadeklarowaniu zmiennej S, typu String, wskaźnik musi uzyskać do niej adres. Realizuje to operator @. Od tego momentu wskaźnik P wskazuje na zmienną S. Taka instrukcja:

P^ := 'Borland Delphi 7 Studio';


W rzeczywistości spowoduje zmianę wartości zmiennej S! Dzieje się tak dlatego, że wskaźnik P wskazuje na zmienną S i zmieniając wartość wskaźnika, w rzeczywistości zmieniamy wartość  zmiennej! Możesz to sprawdzić dodając na końcu programu jeszcze jedną instrukcję:

  MessageBox(0, PChar(S), '', 0);


W okienku informacyjnym będzie widniał napis Borland Delphi 7 Studio.

Do czego to służy?


To jest dobre pytanie! Można się zastanowić: &#8222;Do czego służą wskaźniki?&#8221;. Założenie jest takie, iż podczas tworzenia jakichś struktur tak tablice, czy rekordy nie potrzebne jest manipulowanie wielkimi blokami pamięci &#8211; wystarczy stworzyć wskaźnik na ten rekord i ew. modyfikować w ten sposób dane, zamiast tworzyć kolejną instancję (kopie) rekordu.

Tworzenie wskaźników na rekordy


Właśnie teraz przedstawię Ci przykład tego, o czym mówiłem wcześniej. Podczas tworzenia jakiegoś rekordu wskazane jest utworzenie nowego typu wskaźnikowego, wskazującego na ten rekord. Po wypełnieniu danych do procedury wędruje jedynie wskaźnik na ten rekord:

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.


Wskaźnik odczytuje dane rekordu InfoRec w pamięci &#8211; możliwa jest także zamiana tych danych, co zaprezentowałem w powyższym listingu.

Przydział i zwalnianie pamięci


Na samym początku omawiania wskaźników zaprezentowałem przykład, w którym próbowaliśmy przydzielić dane do wskaźnika. Uruchomienie tamtego programu skończyło się błędem, dlatego, że nie zaalokowaliśmy pamięci dla tych wskaźników. Pamięć można zaalokować (przydzielić) przy pomocy funkcji New.
Stos to cały obszar pamięci rezerwowany dla aplikacji w trakcie jej uruchamiania.

Po wywołaniu funkcji New program automatycznie alokuje w sposób dynamiczny pamięć dla rekordu. Po skończeniu pracy z rekordem pamięć należy zwolnić pleceniem Dispose.

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.


W celu zaalokowania pamięci można posłużyć się także procedurami GetMem/FreeMem. Funkcja GetMem wymaga wpisania dodatkowego parametru jakim jest ilość bajtów do alokacji. Dane te uzyskujemy wywołując funkcję SizeOf &#8211; np.:

GetMem(InfoRec, SizeOf(InfoRec));


Zalecane jest jednak użycie funkcji New/Dispose.

Wrtość pusta


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

Wskaznik := nil;


Taki zapis spowoduje, iż do wskaźnika nie są aktualnie przypisane żadne wartości.

5 komentarzy

marcinsgdansk 2007-04-13 14:46

>>Czy w programie PRecApp, zaprezentowanym w artykule, nie należałoby do pól rekordu odwoływać się również przez '^' ?

I czy w Delphi istnieje jakiś odpowiednik wskaźnika na zmienną dowolnego typu, jak np. w C++? Jeżeli tak, to należałoby o tym też tutaj wspomnieć ;)<<

Jest typ Pointer - predefiniowany typ wskaźnikowy. Zmienne typu Pointer nie wskazują danych żadnego określonego typu, są jednak zgodne z dowolnym innym typem wskaźnikowym.

ROB4L 2006-12-28 22:03

Czy w programie PRecApp, zaprezentowanym w artykule, nie należałoby do pól rekordu odwoływać się również przez '^' ?

I czy w Delphi istnieje jakiś odpowiednik wskaźnika na zmienną dowolnego typu, jak np. w C++? Jeżeli tak, to należałoby o tym też tutaj wspomnieć ;)

Mareczek1 2004-01-22 16:19

ps mam pytanko bo zajmuje sie troche asm jak zapisać daną do pamieci roboczej innego programu zakladam ze przy pomocy softica uda mnie sie wyciągnąć adres chcial bym napisac coś w rodzaju programowego zadajnika ??

Mareczek1 2004-01-22 16:13

nie wiem czy mam racje ale malutka uwaga w pc stos jest podwieszany nie ma to znaczenia w przypadku standardowych operacj na stosie
ale w przypadku odłorzenia 2 bajtów chcąc zdjąć pierwszy musimy zwiększyc wskaźnik stosu o 1 nie zmniejszać jakby to wyglądalo gdyby stos rozumiec w sposób potoczny

Adam Boduch 2004-01-09 17:48

Poprawiony i zmodyfikowany dnia 9 stycznia 2004.