Wskaźniki

Adam Boduch

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

Rejestry

Rejestr to specjalny obszar pamięci wbudowany w CPU (Central Processing Unit). Języki wysokiego poziomu posiadają wygodne funkcje – np. Writeln, które realizują pewne działanie – wyświetlanie tekstu na konsoli. W rzeczywistości jednak jedna instrukcja Writeln odpowiada instrukcjom <wiki href="procesor">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ą – 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 – identycznie działa stos – nie możesz zdjąć żadnych danych ułożonych w środku – 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ć – nie ma to nic wspólnego ze stosem.
FIFO można porównać do kolejki sklepowej – pierwszy klient w kolejce kupuje pierwszy poczym pierwszy wychodzi. To samo można powiedzieć o drukarce – 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 – są one „układane” 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 – np. 1 MB. W tym momencie możesz korzystać z tego 1 megabajta w dowolny sposób, w dowolnym miejscu w programie.

Jest tylko jedna niedogodność – w momencie, gdy dane nie będą Ci już potrzebne – 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 – 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 – 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ć: „Do czego służą wskaźniki?”. Założenie jest takie, iż podczas tworzenia jakichś struktur tak tablice, czy rekordy nie potrzebne jest manipulowanie wielkimi blokami pamięci – 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 – 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 – 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

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.

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ć ;)

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 ??

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

Poprawiony i zmodyfikowany dnia 9 stycznia 2004.