![]() |
Turbo Pascal.
|
![]() |
Więcej pamięci!
Jak być może jeszcze pamiętasz, przystępując do pisania naszego programu bibliotecznego określiliśmy maksymalną liczbę pozycji (rekordów) na około 750. Ograniczenie to wynikało z wielkości dostępnej pamięci, którą z kolei oszacowaliśmy na z grubsza 64 kB. Dlaczego jednak tylko tyle? W chwili obecnej trudno jest znaleźć komputer PC posiadający mniej niż 1 MB pamięci, zaś standardem jest 8 lub 16 MB. Co prawda system operacyjny DOS, pod nadzorem którego pracują programy pascalowe, umożliwia dostęp tylko do 640 kB, ale i tak program
program IlePamieci; begin writeln('Masz w tej chwili ', MemAvail, ' bajtow wolnej pamieci.'); end. pokaże zapewne liczbę większą od 64 kB. Jak zatem dobrać się do pozostałej pamięci?
Zaczniemy od małej dawki teorii. Wszystkie zmienne, którymi posługiwałeś się do tej pory, należały do klasy zmiennych globalnych (jeśli zdefiniowałeś je na początku programu) lub lokalnych (jeżeli zostały zdefiniowane w procedurach). Zmienne globalne umieszczane są w tzw. segmencie danych, którego wielkość wynosi 64 kB i jest stała; zmienne lokalne przechowywane są w segmencie stosu, którego wielkość można zmieniać w granicach od 1 do 64 kB. Zmienne globalne są statyczne, tj. istnieją przez cały czas wykonywania programu, natomiast zmienne lokalne "żyją" tylko tak długo, jak długo wykonują się posiadające je procedury lub funkcje. Koniec końców, na zmienne możemy przeznaczyć co najwyżej 128 kilobajtów, z czego 64 kB dostępne jest "warunkowo". Skoro jednak program IlePamieci pokazał (mam nadzieję...), że pamięci masz nieco więcej, musimy znaleźć sposób na jej wykorzystanie.
Sposobem tym są tak zwane zmienne wskazywane. W odróżnieniu od "zwykłych" zmiennych, zmienne wskazywane mogą być umieszczane w dowolnym miejscu pamięci, z czym wiąże się nieco inny sposób odwoływania do nich, wykorzystujący tzw. wskaźniki.
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:
Rysunek 12. Sposób obsługi zmiennych statycznych (a) i wskazywanych (b)
Jak widać, dostęp do zmiennej wskazywanej odbywa się dwuetapowo, za pośrednictwem wskaźnika: program odczytuje wartość tego ostatniego (czyli adres zmiennej) i na jej podstawie odwołuje się do właściwej zmiennej, położonej w pamięci w miejscu opisanym adresem (w naszym przypadku 3CA6F szesnastkowo). Metoda ta powoduje nieznaczne wydłużenie czasu dostępu do zmiennej, ale pozwala za to umieścić ją praktycznie w dowolnym miejscu pamięci (dokładnie, zmienne wskazywane umieszczane są na tzw. stercie (ang. heap) - wydzielonym obszarze pamięci zarządzanym przez specjalne procedury).
Deklaracja wskaźnika do zmiennej określonego typu jest bardzo podobna do "zwykłej" deklaracji zmiennej i różni się od niej jedynie symbolem wskaźnika (^):
zmienna : ^typ
przy czym typ musi być identyfikatorem typu prostego lub zdefiniowanego wcześniej w sekcji type. Odwołanie do samego wskaźnika wygląda dokładnie tak samo, jak dla każdej innej zmiennej, natomiast odwołanie do wskazywanej przez niego zmiennej zawiera dodatkowo znaczek ^, tym razem położony za nazwą wskaźnika:
zmienna^
A oto kilka przykładów:
type TabReal = array[1..200] of real; { tablica 100 liczb real } PTabReal = ^TabReal; { wskaźnik do tablicy } string100 = string[100]; { skrócony łańcuch } var TabTabReal : array[1..200] of PTabReal; { tablica wskaźników } Pstring100 : ^string100; { wskaźnik do łańcucha } pi : ^integer; { wskaźnik do liczby integer } { ... } TabTabReal[10]^[30] := 12.345; Pstring100^ := 'Lancuch dynamiczny'; writeln(PString100^); Na szczególną uwagę zasługuje tu zmienna TabTabReal, będąca tablicą wskaźników do tablic liczb rzeczywistych. Struktura tego typu pozwala na efektywne przechowanie dwuwymiarowej tablicy składającej się z 200 wierszy (wskazywanych wskaźnikami) zawierających po 200 liczb każdy, czyli w sumie 40 tysięcy liczb o objętości około 240 kB! Umieszczoną nieco niżej instrukcję TabTabReal[10]^[30] := 12.345 można wytłumaczyć jako odwołanie do trzydziestej komórki w dziesiątym wierszu, czyli tablicy wskazywanej przez dziesiąty wskaźnik w tablicy TabTabReal. Jak nietrudno się już domyślić, głównym zastosowaniem wskaźników jest obsługa dużych struktur danych (np. tablic rekordów, czyli katalogów bibliotecznych...). Z tego też względu mały sens ma deklaracja zmiennej pi, gdyż liczba typu integer zajmuje dwa bajty, zaś wskaźnik - cztery.
Warto tu jeszcze wspomnieć, że oprócz wskaźników do obiektów konkretnego typu Turbo Pascal udostępnia również wskaźniki amorficzne, tj. wskazujące na obszar pamięci i nie związane z żadnym konkretnym typem. Wskaźnik taki deklarowany jest słowem pointer (wskaźnik) i jest zgodny pod względem przypisania z innymi typami wskaźnikowymi. Typ pointer i związane z nim operacje wykorzystywane są przez bardziej zaawansowanych programistów do bezpośredniego, "niskopoziomowego" manipulowania zawartością pamięci.
Drugą bardzo ważną i pożyteczną cechą zmiennych wskazywanych jest możliwość ich tworzenia i niszczenia w zależności od potrzeb. Wymaga to co prawda użycia specjalnych procedur, pozwala jednak na znacznie bardziej efektywną gospodarkę pamięcią. W odróżnieniu od statycznych zmiennych globalnych, istniejących przez cały czas wykonywania programu, zmienne wskazywane należą do klasy zmiennych dynamicznych (czyli istnieją dokładnie wtedy, gdy życzy sobie tego programista). Sam proces utworzenia zmiennej dynamicznej polega na zarezerwowaniu odpowiedniego obszaru pamięci i zapamiętaniu adresu tego obszaru we wskaźniku wskazującym na zmienną. Z kolei usunięcie zmiennej powoduje "zwolnienie rezerwacji" (zawartości zmiennej oraz wskaźnika nie są fizycznie niszczone, ale lepiej się już do nich nie odwoływać). Ceną za możliwość swobodnego przydzielania i zwalniania pamięci jest konieczność bezwzględnego inicjalizowania wskaźników, które przed utworzeniem wskazywanej zmiennej mają wartość nieokreśloną (zwykle zero, co odpowiada wskaźnikowi nil, nie wskazującemu na żaden obiekt). Próba odczytu zmiennej wskazywanej przez taki wskaźnik dostarczy "tylko" bezsensownego wyniku, natomiast próba zapisu (w przypadkowe miejsce pamięci!!) może skończyć się zawieszeniem komputera lub zupełnie nieprzewidzianym jego zachowaniem.
Pamiętaj o inicjalizacji wskaźników! 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.
Ostatnią, chyba najrzadziej stosowaną parę tworzą procedury mark i release:
mark(wskaźnik)
release(wskaźnik)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).
Pora na przykłady. Na początek przedstawimy "zbiorczą" demonstrację możliwości obsługi zmiennych dynamicznych.
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.
Na zakończenie tego rozdziału spróbujemy wykorzystać mechanizmy zmiennych dynamicznych do lepszego zagospodarowania pamięci w programie obsługi biblioteki. Podane niżej informacje będą miały z konieczności charakter wskazówek, powinny jednak umożliwić Ci skuteczne wprowadzenie zmian do programu. Przede wszystkim należy zadeklarować odpowiednie struktury danych:
type { ... } PKsiazka = ^Ksiazka; var Katalog : array[1..2000] of PKsiazka;
i usunąć zbędne już definicje stałych określających ograniczenia pamięciowe. Liczba 2000 w deklaracji tablicy Katalog została wybrana arbitralnie (2000 pozycji to około 170 kB). Kolejnym krokiem jest zmodyfikowanie wszystkich procedur korzystających z parametrów typu Ksiazka przez zmianę typu parametru na PKsiazka, np.:
procedure WprowadzDane(var r : PKsiazka);
co umożliwi im operowanie na zmiennych wskazywanych parametrem. Oczywiście w treści tak zmodyfikowanej procedury należy wszystkie odwołania do "zwykłego" parametru zastąpić odwołaniami wykorzystującymi wskaźnik, np.:
with r^ do {... }
Modyfikacja procedury sortującej jest zbędna, a nawet niepożądana. W nowym programie zamiana rekordów przyjmie formę zamiany wskaźników, co oczywiście będzie szybsze i bardziej efektywne niż kopiowanie całych rekordów.
Wszelkie wywołania procedur pozostaną niezmienione, chociaż faktycznie zamiast rekordu zawsze przekazywany będzie wskaźnik do rekordu. Natomiast wszystkie bezpośrednie odwołania do pól rekordów zapisanych w tablicy Katalog należy zastąpić odwołaniami wykorzystującymi wskaźniki:
Katalog[LbPoz]^.Licznik := 0;
Nie wolno również zapomnieć o uzupełnieniu procedur tworzących i usuwających rekordy o wywołania procedur new i dispose, których brak spowoduje najpewniej błyskawiczne zawieszenie komputera:
procedure DodajKsiazke; { ... } writeln('Nowa pozycja w katalogu: ', Licznik); new(Katalog[Licznik]); { utwórz nowy rekord w katalogu } { ... } procedure UsunKsiazke(Numer : integer); begin dispose(Katalog[Numer]); { ... } Podobnie musisz postąpić z procedurą OdczytajZDysku (w procedurze zapisu usuwanie rekordów nie jest konieczne, ale przydzielona pamięć powinna zostać zwolniona przed zakończeniem działania programu).
Powyższe wskazówki nie obejmują operacji pomocniczych, jak np. sprawdzanie możliwości utworzenia kolejnego rekordu. Powinny one jednak wystarczyć Ci do skutecznego zmodyfikowania programu.
Zapamiętaj
- 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.
Poprzedni | Spis treści | Następny | Wersja spakowana |