Pascal
Wskaźniki, procedury
dynamicznego
przydziału pamięci
Wskaźniki
• Wskaźniki to adresy pamięci,
które pokazują komputerowi
miejsce gdzie znajdują się
jakieś dane. Możesz
przykładowo przechowywać
w pamięci wskaźnik na
obrazek bitmapowy, podczas
gdy przechowywanie obrazu
w tablicy byłoby niezbyt
praktyczne. Pamięć do której
odnoszą się wskaźniki można
w każdej chwili zwalniać,
deklarować i zmieniać.
Budowa wskaźników*
• Ze względu na ryzoko zawieszenia programu wskaźniki są
traktowane jako typy, które należy umiejętnie stosować.
Wskaźniki na ogół zbudowane są z 4 bajtów. Pierwsze 2
określają segment danych, kolejne 2 ofset.
•
Pamięć konwencjonalna (pierwszy 1MB pamięci
komputera) podzielony jest na segmenty po 16KB każdy.
Tak więc 1 segment ma adres 0, drugi 1, trzeci 2, itd...
Ofset to przemieszczenie względem segmentu. Dzięki
ofsetowi możemy dojść do każdego jednego bajtu pamięci.
Adres typu pointer składa się z dwóch bajtów segmentu,
dwóch ofsetu i jest najczęściej zapisywany w kodzie
szesnastkowym, dzięki czemu jest bardziej czytelny, np.
•
$a000:00, $b800:00
Typ pointer
• Typ wskaźników nazywa się
pointer. Gdy jakąś zmienną
zadeklarujesz jako poiner, będzie
ona wskaźnikiem. Wskaźniki
mogą adresować różne typy
zmiennych. Mogą adresować
liczby, napisy, tablice, itd. Gdy
adresują konkretny typ, nie
deklaruje się ich jako pointer,
tylko tworzy swój własny typ
Wskaźnik do zmiennej jest po prostu jej adresem, czyli liczbą
opisującą jej położenie w pamięci. Różnicę pomiędzy zmienną
statyczną a wskazywaną ilustruje poniższy rysunek
Sposób obsługi zmiennych statycznych (a) i
wskazywanych (b)
Podstawianie wskaźników
• Żeby wskaźnik wskazywał adres
jakiejś zmiennej, możemy
napisać:
var
wskaznik : pointer; {lub
podobny typ}
{...}
wskaznik :=
@nazwa_zmiennej;
Znacznik @ oznacza pobranie adresu
zmiennej. Gdybyśmy chcieli teraz coś
zapisać pod wskazanym miejscem
wystarczy wywołać wskazywane miejsce,
za pomocą znaku ^
wskaznik^ := nowa_wartosc;
W podanym przykładzie
trzeba by wskaźnik był
zadeklarowany
jako
wskaźnik
na
liczbę.
Gdybyśmy bez rzutowania
i bez określenia rodzaju
wkaźnika próbowali mu
podstawić
wartość
wystąpiłby błąd "illegal
assigment".
Każdy typ danych w Pascalu ma
odpowiadający mu typ wskaźnikowy. Jest to
typ, który nazywa się tak samo jak typ,
któremu odpowiada, tylko poprzedzony jest
znakiem '^'. Na przykład dla
typu Integer jest to ^Integer. Zmienna
takiego typu może przechowywać adres
pewnych danych typu Integer w pamięci -
dalej będziemy pisać, że zmienna "wskazuje
na coś", ponieważ jest ona jakby strzałką
wskazującą jakieś miejsce w pamięci.
Zadeklarowanie zmiennej pamiętającej
adres oraz przypisanie jej adresu innej
zmiennej będzie wyglądać następująco:
var zmienna: Integer;
adres_zmiennej: ^Integer;
begin
zmienna := 15;
adres_zmiennej := @zmienna; { * }
end.
Gdy już jakiejś zmiennej wskaźnikowej przypiszemy adres,
możemy tego adresu użyć, aby zobaczyć co pod tym adresem
się znajduje. Również do tego służy operator '^', jednak tym
razem stawiany jest za nazwą zmiennej. Tak więc zmienna
'adres_zmiennej' służy to zapamiętania gdzie w pamięci
znajduje się pewna zmienna, a za pomocą wyrażenia
'adres_zmiennej^' możemy sprawdzić jaka wartość się tam
znajduje (jaka jest wartość wskazywanej zmiennej).
var zmienna: Integer;
adres_zmiennej: ^Integer;
begin
zmienna := 15;
adres_zmiennej := @zmienna; { * }
writeln(zmienna);
writeln(adres_zmiennej^);
end.
Dwie ostatnie linijki wypiszą dokładnie to samo
(liczbę 15), ponieważ:
- zmienna ma wartość 15, gdyż taka liczba
została jej przypisana,
- adres_zmiennej pokazuje na miejsce w
pamięci, w którym znajduje się zmienna, a
operator '^' powoduje pobranie znajdującej się
tam wartości.
Procedury
Turbo Pascal oferuje kilka metod tworzenia i usuwania
zmiennych dynamicznych, z których najpopularniejszą
realizuje para procedur new i dispose:
new(wskaźnik-do-zmiennej)
dispose(wskaźnik-do-zmiennej)
Procedura new wykonuje czynności związane z
utworzeniem zmiennej wskazywanej, natomiast dispose -
operacje związane z jej usunięciem. Drugą parę
zarządzającą dynamicznym przydziałem pamięci tworzą
procedury GetMem i FreeMem:
GetMem(wskaźnik, rozmiar-
bloku)
FreeMem(wskaźnik, rozmiar-
bloku)
W odróżnieniu od pary new-dispose, procedury te
wykorzystują wskaźniki amorficzne (typu pointer) i
służą do bardziej "wewnętrznego" manipulowania
pamięcią, tj. przydzielania i zwalniania bloków bajtów
(a nie zmiennych wskazywanych jakiegoś konkretnego
typu). Wielkość przydzielanego lub zwalnianego bloku
(w bajtach) określa parametr rozmiar-bloku.
Korzystając z obu grup procedur musisz pamiętać, że
pamięć przydzielona przez GetMem nie może być
zwolniona procedurą dispose i odwrotnie.
mark(wskaźnik)
release(wskaźn
ik)
Wykonanie procedury mark nie powoduje
przydzielenia pamięci ani utworzenia zmiennej, a
jedynie zapamiętanie bieżącej "wysokości" sterty w
zmiennej wskaźnik. Zwolnienia całego obszaru sterty
leżącego powyżej wskaźnika dokonuje się za pomocą
procedury release. Obydwie procedury stosowane są -
podobnie jak GetMem i FreeMem - głównie w
programowaniu niskiego poziomu, do "masowego"
zwalniania pamięci przydzielonej na stercie
Korzystając ze zmiennych dynamicznych
musisz pamiętać, że sterta nie jest
automatycznie porządkowana, toteż kolejne
operacje przydzielenia i zwolnienia bloków
pamięci (dowolną metodą) mogą
doprowadzić do tzw. fragmentacji, czyli
rozbicia wolnego jeszcze obszaru pamięci
na mniejsze, rozłączne bloki. Ponieważ
rozmiar tworzonej zmiennej dynamicznej
nie może być większy od rozmiaru
największego wolnego bloku pamięci, może
się okazać, że próba utworzenia zmiennej
skończy się niepowodzeniem, mimo iż
wielkość dostępnej pamięci będzie
wystarczająca. Dlatego też właściwą miarą
możliwości utworzenia większej struktury
danych na stercie jest nie
funkcja
MemAvail
(zwracająca sumaryczny
rozmiar wolnej pamięci),
lecz
MaxAvail
(zwracająca rozmiar
największego wolnego bloku).
program ZmienneDynamiczne;
type TabReal = array[1..5000] of real;{ tablica
liczb } { rzeczywistych }
PString = ^string; { wskaźnik do łańcucha }
var
s : PString; { zmienna typu wskaźnik do
łańcucha }
TabTabReal : array[1..100] of ^TabReal;
{ tablica } { wskaźników }
Sterta : pointer; { wskaźnik wysokości sterty }
i : integer; { pomocniczy licznik }
procedure IlePamieci;
begin
writeln('Wolne: ', MemAvail, ' max. blok: ',
MaxAvail, '
bajtow.');
end;
begin
writeln(s^);{ zmienna nie utworzona }
new(s);{ więc ją tworzymy }
writeln(s^);{ utworzona, lecz nie zainicjalizowana }
s^ := 'No wreszcie!'; { inicjalizujemy }
writeln(s^);{ teraz jest OK }
dispose(s);{ usuwamy }
writeln(s^);{ zmienna nie została całkowicie zniszczona! }
mark(Sterta);{ zaznaczamy 'poziom' sterty }
i := 1;{ tworzymy tablicę tablic dynamicznych }
while MemAvail > SizeOf(TabReal) do { tyle wierszy ) { ile się
da }
begin
IlePamieci; { ile mamy pamięci? }
new(TabTabReal[i]); { tworzymy nowy wiersz }
Inc(i); { zwiększamy indeks wiersza }
end;
dispose(TabTabReal[3]); { usuwamy jeden wiersz tablicy }
IlePamieci;
release(Sterta); { zwalniamy hurtem całą pamięć }
IlePamieci;
end.
Pierwsza część programu demonstruje etapy tworzenia,
wykorzystania i usunięcia zmiennej wskazywanej (w
naszym
przypadku
łańcucha)
za
pomocą
procedur new i dispose.
Zauważ,
że
utworzenie
zmiennej wskazywanej nie jest równoznaczne z jej
inicjalizacją, a po wykonaniu procedury dispose treść
łańcucha nie jest niszczona, chociaż może być
niekompletna.
Druga część tworzy typową strukturę wielkiej tablicy,
przydzielając pamięć dla poszczególnych wierszy,
dopóki to jest możliwe. Zauważ, że po usunięciu
trzeciego wiersza tablicy na ogół okazuje się, że rozmiar
największego dostępnego bloku jest mniejszy od
całkowitego rozmiaru wolnego obszaru sterty, co
uniemożliwia
tworzenie
większych
struktur
dynamicznych. Wreszcie instrukcja release zwalnia całą
stertę "wzwyż" począwszy od miejsca zarejestrowanego
w zmiennej Sterta.
Zapamietaj!
- Do przechowywania większych ilości danych możesz w
Pascalu wykorzystać zmienne wskazywane (dynamiczne).
- Zmienne wskazywane są umieszczane na tzw. stercie
(teoretycznie w dowolnym miejscu pamięci). Mogą one być
tworzone i niszczone dynamicznie, w zależności od potrzeb.
- Zmienna wskazywana lokalizowana jest za pomocą
wskaźnika, który zawiera jej adres (miejsce w pamięci).
Wskaźniki mogą wskazywać na zmienne konkretnego typu,
mogą też być wskaźnikami amorficznymi (pointer).
- Przed wykorzystaniem zmiennej dynamicznej należy ją
utworzyć (procedurą new), a po wykorzystaniu - usunąć
(procedurą dispose).
- Do przydzielania i zwalniania bloków pamięci na stercie
służą również procedury GetMem, FreeMem, mark i release.
Autor:
Michał Lis
3si