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 ? np. Delphi czy C. Niestety na platformie .NET wskaźniki są tzw. elementem niebezpiecznym (ang. unsafe) i korzystanie z nich nie jest możliwe. Istnieją, co prawda, wyjątki od tej reguły i są znane pewne sposoby na ominięcie tego problemu (patrz Elementy języka Delphi), ale Microsoft ani Borland nie zalecają ich używania. Dla niektórych nie jest to zbyt pocieszające ? wszak wskaźniki były istotnym elementem programowania, wielu więc straciło tym samym potężne narzędzie do manipulowana danymi i pamięcią.

Jednak wskaźniki mogą być w dalszym ciągu wykorzystywane na platformie Win32, oczywiście przy użyciu Delphi.

1 Rejestry
2 Stos
     2.1 LIFO
     2.2 FIFO
3 Sterta
4 Do czego służą wskaźniki?
5 Tworzenie wskaźnika
6 Przydział danych do wskaźników
     6.3 Uzyskiwanie adresu pamięci
7 Tworzenie wskaźników na struktury
8 Przydział i zwalnianie pamięci
9 Wartość pusta

Warto więc dodać, że wskaźniki były jedną z najczęstszych (o ile nie najczęstszą) przyczyną występowania problemów w aplikacjach. Wymagały bowiem operowania na pamięci, co często kończyło się błędami typu AccessViolation.

Rejestry

Rejestr jest specjalnym obszarem pamięci wbudowanym w CPU (Central Processing Unit). Języki wysokiego poziomu zapewniają wygodne funkcje, jak na przykład Writeln, która realizują pewne działanie ? wyświetlanie tekstu na konsoli. W rzeczywistości jednak jedna instrukcja Writeln odpowiada kilku instrukcjom procesora.

Istnieje także coś takiego jak wskaźnik instrukcji, kontrolujący, która instrukcja zostanie wykonana jako następna. W ten sposób działa program, który jest wykonywany przez procesor.

Stos

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

LIFO

LIFO oznacza Last In, First Out (ostatni wchodzi, pierwszy wychodzi). LIFO jest rodzajem kolejki ? algorytmem operacji na stosie: stanowi uporządkowaną strukturę liniową. Podczas wykonywania jakiejś funkcji programu, na stos są odkładane pewne dane (na sam wierzch), po czym ? po wykonaniu operacji ? są od razu zdejmowane.

Aby ułatwić zrozumienie tego pojęcia, często podaje się przykład z monetami lub z talerzami: układając monety jedną na drugiej, tworzymy z nich wieżę. Teraz, aby zdjąć wszystkie elementy tej wieży, trzeba zacząć od monety ułożonej na samej górze. Identycznie działa stos ? nie można zdjąć żadnych danych ułożonych w środku, lecz trzeba zacząć od tych na samym wierzchu.

FIFO

Innym mechanizmem kolejkowania jest FIFO ? First In, Fist Out (pierwszy wchodzi, pierwszy wychodzi). Wspominam o tym jako o ciekawostce, z którą możesz także może się spotkać ? algorytm ten nie ma nic wspólnego ze stosem.

FIFO można porównać do kolejki sklepowej ? pierwszy klient w kolejce kupuje pierwszy, po czym pierwszy wychodzi. To samo można powiedzieć o drukarce ? najpierw drukuje ona pierwsze strony, potem kolejne, aż w końcu drukuje ostatnią i kończy działanie.

Sterta

Stos jest pamięcią alokowaną w momencie działania programu. Na przykład parametry funkcji oraz zmienne lokalne są układane na stosie w momencie wywołania funkcji, natomiast po jej zakończeniu jej działania są ze stosu zdejmowane.

Sterta stanowi całą dostępną 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żna w trakcie działania programu zarezerwować sobie pewien blok pamięci ? np. 1 MB. W tym momencie ten 1 megabajt może być wykorzystywany w dowolny sposób, w dowolnym miejscu w programie.

Jest tylko jedna niedogodność ? gdy dane przestaną już być potrzebne, programista samodzielnie musi zadbać o zwolnienie pamięci.

Do czego służą wskaźniki?

Ogólnie mówiąc, służą do manipulowania danymi. Dzięki wskaźnikom mogliśmy zaalokować dowolny obszar pamięci na stercie. Wskaźniki umożliwiały również pobranie adresu miejsca w pamięci, w jakim znajduje się dana zmienna, a także manipulację tymi danymi. Programiści języka C/C++ lub innego języka, w którym wskaźniki są nieodłączonym elementem, znają prezentowane zagadnienia i w tym momencie zapewne są zadowoleni z faktu, że wskaźniki w Delphi dla .NET nie istnieją (jeżeli nie wiedzieli o tym wcześniej).

Tworzenie wskaźnika

Na samym początku muszę zaznaczyć, że prezentowane przeze mnie przykładowe fragmenty kodu działają poprawnie jedynie w środowisku Win32 (na platformie .NET istnieją pewne ograniczenia co do stosowania wskaźników - patrz Elementy języka Delphi).

Jedynym ze sposobów przekazywania parametrów do procedury/funkcji jest przekazywanie przez referencje, co polega na umieszczeniu przed nazwą parametru słowa kluczowego Var. W takim przypadku zawartość zmiennej nie jest kopiowana do funkcji, tak jak to ma miejsce w przypadku przekazywania parametru przez wartość. Przekazywany jest jedynie adres komórki w pamięci, w której są umieszczone dane. Spójrzmy na poniższy przykład:

program Pointers;

{$APPTYPE CONSOLE}

procedure Foo(var Value : String);
begin
  Value := Value + ' jest fajne';
end;

var
  S : String;
begin
  S := 'Delphi';
  Foo(S);

  Writeln(S);
  Readln;
end.

Na samym początku zadeklarowałem zmienną S, do której przypisałem wartość Delphi. Następnie zmienną S przekazano jako parametr do procedury Foo. W procedurze Foo zmodyfikowałem parametr Value, nadając mu nową wartość. W rzeczywistości zarówno S, jak i Value wskazują na tę samą komórkę pamięci, tak więc przez modyfikację wartości Value zmieniłem również wartość zmiennej S.

Przekazywanie parametrów przez referencję jest szybsze i zabiera mniej pamięci.
Wskaźniki są zmiennymi, które wskazują na inną zmienną.

Zapewne powyższa wskazówka niewiele wyjaśnia. Wskaźniki są specjalnym typem danych ? w pamięci nie są przechowywane dane (zawartość zmiennej), lecz jedynie odpowiadające im adresy komórki pamięci.
Zadeklarowanie wskaźnika następuje za pomocą operatora (^).

var
  P : ^String;

Od tego momentu w programie można korzystać ze wskaźnika P, wskazującego na typ String. We wszelkich operacjach dokonywanych na wskaźnikach muszą być wykorzystywane dwa operatory specyficzne jedynie dla typów wskaźnikowych ? są to operatory ^ oraz @. Ich znaczenie opiszę w dalszej części tego tekstu.

Przydział danych do wskaźników

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

program Pointers;

var
  P : ^String;

begin
  P^ := 'Delphi';
end.

Program próbuje przypisać określone dane do wskaźnika w postaci ciągu tekstowego. Konieczne tu jest wykorzystanie operatora ^, w przeciwnym wypadku Delphi zasygnalizuje błąd: [Error] Pointers.dpr(12): Incompatible types: 'String' and 'Pointer'.

Próba uruchomienia takiego programu zakończy się jednak błędem typu Runtime

Przyczyną zaistnienia błędu jest fakt, że do typu wskaźnikowego nie można przypisać wartości w normalny sposób. Wskaźniki muszą uprzednio wskazywać na inną, zwykłą zmienną.

O przydzielaniu danych bezpośrednio do wskaźnika opowiem się w dalszej części tego tekstu.

Poniższy program zostanie skompilowany i, co najważniejsze, będzie działał bez problemu:

program Pointers;

{$APPTYPE CONSOLE}

var
  S : String;
  P : ^String;

begin
  S := 'Delphi'; // przypisanie danych do zwykłej zmiennej
  P := @S; // uzyskanie adresu zmiennej
  P^ := 'Delphi jest fajne'; // modyfikacja danych

  Writeln(S);
  Readln;
end.

Po uruchomieniu takiego programu na konsoli zostanie wyświetlony napis Delphi jest fajne, mimo iż odwołano się do zmiennej S, która uprzednio miała wartość Delphi.

Uzyskiwanie adresu pamięci

Operator @ stosuje się do uzyskania adresu komórki pamięci, w której znajduje się wartość danej zmiennej (w tym wypadku - S). Następnie przypisując dane do zmiennej wskaźnikowej P, można zmienić jednocześnie wartość zmiennej S.

Tworzenie wskaźników na struktury

Można się zastanawiać, do czego służą wskaźniki? Założenie jest takie, że podczas tworzenia jakichś struktur ? zarówno tablic, jak i rekordów ? nie jest konieczne manipulowanie wielkimi blokami pamięci. Wystarczy tylko utworzyć wskaźnik tego rekordu i ewentualnie modyfikować w ten sposób dane, zamiast tworzyć kolejną instancję (kopię) rekordu.

Podczas tworzenia jakiegoś rekordu wskazane jest utworzenie nowego typu wskaźnikowego, wskazującego na ten rekord. Po wypełnieniu danych następuje przekazanie do procedury jedynie wskaźnika tego rekordu:

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.

Spójrzmy na powyższy przykład. Na samym początku zadeklarowałem rekord, do którego przydzieliłem dane ? informacje na temat osoby Jana Kowalskiego. Następnie do procedury SomeProc przekazałem adres komórki pamięci, w której jest umiejscowiony rekord. Procedura SomeProc, znajdując adres komórki pamięci, modyfikuje dane rekordu.

Przydział i zwalnianie pamięci

Na samym początku tego tekstu zaprezentowałem przykład, w którym próbowałem przydzielić dane do wskaźnika. Uruchomienie tamtego programu skończyło się błędem z powodu próby przypisania danych do zmiennej wskaźnikowej, mimo iż nie pobrano adresu komórki (za pomocą operatora @). Istnieje możliwość dynamicznej alokacji pamięci w trakcie działania programu. Służy do tego funkcja New oraz Dispose. Oto przykład:

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.

Jak widać, w tym przykładzie nie pobrano adresu komórki żadnego rekordu. Zwyczajnie, na samym początku została zaalokowana (funkcja New) pamięć na potrzeby naszej zmiennej wskaźnikowej (InfoRec), aby później przypisać dane.

W celu zaalokowania pamięci można posłużyć się także funkcjami GetMem i FreeMem. Funkcja GetMem wymaga wpisania dodatkowego parametru, jakim jest liczba bajtów przeznaczonych do alokacji. Dane te uzyskuje się, wywołując funkcję SizeOf ? np.:

GetMem(InfoRec, SizeOf(InfoRec));

Zalecane jest jednak użycie funkcji New i Dispose.

Wartość pusta

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

Wskaźnik := nil;

Taki zapis spowoduje, że do wskaźnika nie będą aktualnie przypisane żadne wartości.

Zobacz też:

4 komentarzy

Przy próbie skompilowania programu z punktu "Tworzenie wskaźników na struktury" dostaję komunikaty o błędach. Między innymi:
18,71 Illegal qualifier. //dla wartości FName w procedurze.
18,61 Operator is not overloaded: "Constatn String" + PInfoRec";
18,71. Syntax error, ")" expected but "identifier FNAME" found;

Wszystko jest dokładnie tak samo jak w listingu, nawet jak robię kopiuj-wklej.

  1. SomeProc(@InfoRec);

           Czy zamiast wskaźników można po prostu użyć var lub const?
    
  2. var x: Pointer;
    begin
    x := AllocMem(20);
    StrPCopy(x,'Ala ma kota');
    FreeMem(x,20);
    end;

Czy FreeMem(x,20) mogę zastąpić x := nil - czy będzie ten sam efekt to znaczy, czy pamięć zostanie zwolniona, czy może aby wskaźnik przestanie wskazywać na wartość, a ona sama będzie nadal zaalokowana?

A od kiedy to w PHP mamy wskaźniki? :>

Hmm... czy niczego nie pomieszałem? Artykuł o tym samym tyule jest w dziale Artykuly... nie wiem co z nim zrobic - usunac?