Pamięć dynamiczna

From Studia Informatyczne

Spis treści

Pamięć dynamiczna

Dotychczas używając pamięci trzeba było z góry znać jej wielkość, aby zadeklarować odpowiednią strukturę danych. Takie rozwiązanie bywa niewygodne, gdy nie wiemy z jak dużymi danymi będziemy mieli do czynienia. Często bowiem wraz z danymi pojawia się ich rozmiar i dopiero wtedy chcielibyśmy ustalać wielkość struktury danych. W tym celu użyjemy tzw. pamięci dynamicznej, czyli takiej, której rezerwacja i zwalnianie odbywa się w czasie działania programu.

System operacyjny, uruchamiając program, przeznacza na jego obsługę fragment pamięci zapewniając wyłączność tak, aby żaden inny proces nie mógł się do niej dostać. W tym fragmencie przydzielonej pamięci mieści się kod programu, dane statyczne (czyli zadeklarowane na etapie deklaracji) oraz dane dynamiczne, czyli takie, dla których pamięć jest rezerwowana i zwalniana na bieżąco. W naszym języku będzie można deklarować zmienne dynamiczne, które będą rozpoznawane dzięki poprzedzeniu typu przy deklaracji symbolem ^, na przykład deklaracja

var px:^Integer;

będzie traktowana jako deklaracja zmiennej wskaźnikowej do obiektu typu całkowitego.

Alokacja i zwalnianie pamięci dynamicznej

Przydział (alokacja) i zwalnianie (dealokacja) pamięci dynamicznej odbywa się za pomocą dwóch procedur: New(z) oraz Dispose(z). Obie te procedury mają jeden argument: zmienną. Wykonanie procedury New(z) powoduje następujące czynności

1. Określenie wielkości elementu alokowanego typu (wielkość ta musi być znana na etapie kompilacji
   programu)
2. Znalezienie adresu w pamięci, począwszy od którego jest dostępnych tyle kolejnych komórek, 
   ile potrzeba na zaalokowanie żądanego elementu
3. Zarezerwowanie alokowanego obszaru
4. Przypisanie znalezionego adresu zmiennej z

Jeśli nie ma spójnego fragmentu pamięci, w którym można by zaalokować żądany element, to procedura New(z) powoduje błąd.

Procedura Dispose(z) powoduje tylko jedną rzecz

1. Odblokowanie zarezerwowanego obszaru pamięci i zwrócenie go do puli niewykorzystanych obszarów.

Przyjmijmy, że po wykonaniu Dispose(z) wartość zmiennej z jest nieokreślona. W rzeczywistości często kompilatory po zwolnieniu zaalokowanego obszaru pozostawiają starą wartość adresu na zmiennej, której dealokacja dotyczyła. Bardzo niebezpieczne jest używanie tej wartości do czegokolwiek i nie należy nigdy tego robić.

Zatem zmienne dynamiczne operują w rzeczywistości na adresach. W konsekwencji pamięć rezerwowana na zmienną dynamiczną jest taka sama (zazwyczaj 4 bajty) i niezależna od tego, na co ta zmienna wskazuje.

Operacje na zmiennych dynamicznych

Zmienne dynamiczne mają ograniczone możliwości wykonywania na nich działań. Możliwe jest porównanie ich wartości, ale tylko pod kątem równości, czyli z operatorów relacyjnych mamy jedynie dwa, a mianowicie = oraz <>. Nie wolno zatem porównywać zmiennych dynamicznych za pomocą operatorów <,<=,>,>=.

Zmiennym dynamicznym można przypisywać wartości, ale jest to ograniczone do innych zmiennych dynamicznych; nie można budować za ich pomocą żadnych wyrażeń. Odwołanie się do wartości elementu wskazywanego przez zmienną dynamiczną odbywa się za pomocą operatora ^. Zatem

z^, to element wskazywany przez zmienną z

Należy bardzo uważać, żeby odróżniać przypisanie z:=x od przypisania z^:=x^. W pierwszym przypadku nastąpi przypisanie zmiennej z wartości adresu wskazywanego przez zmienną x. W drugim przypadku nastąpi skopiowanie wartości elementu wskazywanego przez zmienną x elementowi wskazywanemu przez zmienną z. Przyjmijmy w poniższych przykładach, że zmienne x oraz z są zadeklarowane jako var x,z:^Integer. Wykonanie następującej sekwencji instrukcji

New(x);
z:=x;
z^:=5;
Write(x^)

spowoduje wypisanie wartości 5, gdyż obie zmienne wskazują na ten sam obszar pamięci.

Jeśli natomiast wykonalibyśmy następującą sekwencję

New(x); 
New(z);
z^:=5;
x^:=z^;
z^:=3;
Write(x^);

to wypisałaby się wartość 5, gdyż każda z naszych zmiennych wskazuje na inny obszar pamięci i działa na nim autonomicznie. Zastanówmy się, co stałoby się, gdybyśmy tę sekwencję zakończyli przypisaniem z:=x.

Niebezpieczeństwa stosowania zmiennych dynamicznych

Zmienne dynamiczne stwarzają niebezpieczeństwo nieudolnego wykorzystania tego mocnego mechanizmu programistycznego. Wykonanie ostatniego przypisania w powyższym przykładzie spowodowałoby uratę zaalokowanego obszaru pamięci dla zmiennej z. Po prostu zmienna z po przypisaniu z:=x wskazywałaby na ten sam obszar pamięci, co zmienna x i jakiekolwiek odwołanie do x byłoby teraz równoważne odwołaniu do z (włącznie z Dispose). Pamięć przydzielona wcześniej zmiennej z pozostałaby od tej pory niedostępna. Co gorsza, nie udałoby się już nam jej zwolnić, bowiem wykonanie instrukcji Dispose(z) spowodowałoby zwolnienie pamięci przydzielonej zmiennej x! Obszaru przydzielonego zmiennej z nie wskazywałaby już żadna zmienna i nie można byłoby go zwolnić.

Tego typu działanie powoduje powstanie groźnego w skutkach zaśmiecenia pamięci. System operacyjny bowiem będzie rezerwował zajęty obszar do końca działania programu, nie zauważając, że nikt z tego obszaru nie jest już w stanie skorzystać.

Równie groźne są efekty wzajemnego działania na tym samym obszarze. Wyobraźmy sobie następującą sekwencję instrukcji.

New(x);
x^:=5;
z:=x; 
...
Dispose(z);
...
New(z); 
z^:=3;
Write(x^);

Efekt działania tego kodu jest nieokreślony. Może się zdarzyć, że zostanie wypisana piątka, może trójka (jeśli przez przypadek zmienna z dostałaby ten sam obszar, który wcześniej zwolniła), a może jeszcze coś innego, jeśli po wykonaniu Dispose(z) jakaś inna zmienna dostałaby zwolniony obszar i coś tam w nim zapisała.

Tego typu błędy są trudne do wykrycia, bo w testach w ogóle mogą nie zostać zauważone. Niedeterministyczny charakter alokacji daje tu ogromną gamę możliwych wyników i nie sposób przewidzieć, jak w konkretnym przebiegu programu będą wyglądały wyniki.