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ż:
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.
SomeProc(@InfoRec);
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?