![]() |
Turbo Pascal.
|
![]() |
Do dzieła!
Po tym nieco przydługim teoretyzowaniu większość Czytelników zapewne świerzbią już ręce, żeby wziąć się do programowania. W niniejszym rozdziale spróbujemy przedstawić podstawowe elementy składowe naszego programu i powiązać je w działającą całość. Zaczniemy od rzeczy podstawowych, czyli zdefiniowania struktur danych, na których program będzie operował.
program Biblioteka; { Prosty program do obsługi biblioteki } uses Crt; { moduł obsługujący klawiaturę i monitor } type string30 = string[30]; { potrzebne do przekazywania } { parametrów } string25 = string[25]; { typu łańcuchowego do procedur } Ksiazka = record { rekord opisujący książkę } Tytul : string30; Autor : string25; Wypozyczajacy : string25; Licznik : word; end; const MAX_PAMIEC = 63000; { maksymalna wielkość katalogu } { w bajtach } POJEMNOSC = MAX_PAMIEC div SizeOf(Ksiazka); var Katalog : array[1..Pojemnosc] of Ksiazka; { właściwy } { katalog } LbPoz : integer; { liczba pozycji w katalogu } Podstawowym warunkiem użyteczności katalogu jest możliwość wprowadzania do niego informacji (z klawiatury) i wyprowadzania jej (na ekran/drukarkę). Ponieważ podstawową jednostką organizacyjną katalogu jest rekord, powinniśmy zdefiniować procedury pozwalające na wprowadzenie i wyprowadzenie jego zawartości. W myśl tego, co powiedzieliśmy poprzednio, procedury te powinny operować na parametrze odpowiedniego typu rekordowego:
procedure WprowadzDane(var r : Ksiazka); { wprowadza pojedynczy rekord z klawiatury } begin with r do { wprowadź poszczególne pola rekordu } begin write('Tytul: '); readln(Tytul); write('Autor: '); readln(Autor); write('Wypozyczajacy: '); readln(Wypozyczajacy); end; end; procedure WypiszDane(r : Ksiazka); { wyprowadza zawartość rekordu na ekran } begin with r do { wyprowadź poszczególne pola rekordu } begin write('Tytul: '); writeln(Tytul); write('Autor: '); writeln(Autor); if Wypozyczajacy = '' then { nikt nie wypożyczył } writeln('Ksiazka znajduje sie na polce.') else { pole zawiera nazwisko wypożyczającego } write('Wypozyczajacy: '); writeln(Wypozyczajacy); end; writeln; end; Mimochodem przyjęliśmy konwencję, w myśl której książce nie wypożyczonej odpowiada puste pole Wypozyczajacy (co jest dość naturalne). Powyższe definicje wykorzystamy do utworzenia kilku dodatkowych procedur pozwalających na wprowadzenie opisu nowej książki do katalogu oraz wyprowadzenie całej zawartości katalogu na ekran (co w przypadku większej liczby książek jest raczej mało sensowne, chyba że zamiast ekranu użyje się drukarki). Przy okazji warto też zdefiniować procedurę pozwalającą na usunięcie książki z katalogu, co najprościej zrobić przez przepisanie ostatniego elementu katalogu w miejsce elementu usuwanego.
procedure DodajKsiazke; { dodaje nową książkę do katalogu } begin Inc(LbPoz); { dodajemy nową książkę } writeln('Nowa pozycja w katalogu: ', LbPoz); WprowadzDane(Katalog[LbPoz]); { wprowadź dane } { nowej książki } Katalog[LbPoz].Licznik := 0; { książka nie była } { wypożyczana } end; procedure UsunKsiazke(Numer : integer); { usuwa książkę z katalogu } begin Katalog[Numer] := Katalog[LbPoz]; { zamazuje daną pozycję } Dec(LbPoz); { usunięto książkę } end; procedure WypiszKatalog; { wypisuje całą zawartość katalogu na ekran } var i : integer; begin for i := 1 to LbPoz do begin writeln('Pozycja katalogu nr ', i,':'); WypiszDane(Katalog[i]); end; end; Porządny program powinien również zapewniać możliwość zmiany opisu książki; konstrukcję odpowiedniej procedury pozostawiam Ci jako ćwiczenie (zobacz też poniżej).
Kolejną sprawą jest obsługa wypożyczenia i zwrotu książki. W tym celu należy po pierwsze odszukać wybraną książkę (wedle wcześniejszych założeń - według tytułu), a następnie, w zależności od potrzeb, wprowadzić, sprawdzić lub usunąć nazwisko wypożyczającego. Jeśli chodzi o wyszukiwanie książki, to sensownym wydaje się zaprojektowanie funkcji, której argumentem będzie zadany fragment tytułu, a wartością zwracaną - numer książki w katalogu lub -1, jeśli książki nie znaleziono (numery książek są zawsze dodatnie, a więc liczba -1 nigdy nie będzie opisywała rzeczywiście istniejącej w katalogu książki). Ponieważ katalog może zawierać kilka książek o tym samym tytule, musimy zaprojektować funkcję wyszukującą tak, by umożliwiała znalezienie wszystkich "podejrzanych". W tym celu wyposażymy ją w możliwość szukania od zadanej pozycji:
function Szukaj(Tekst : string; Pozycja : integer) : integer; { wyszukuje książkę o tytule zawierającym Tekst } { szuka od zadanej pozycji katalogu } var i : integer; begin i := Pozycja; { szukaj od zadanej pozycji } { sprawdź, czy tytuł zawiera tekst } { i czy przeszukano cały katalog } while (Pos(Tekst, Katalog[i].Tytul) = 0) and (i <= LbPoz) do Inc(i); if i <= LbPoz then { znaleziono odpowiednią } { pozycję katalogu } Szukaj := i else { nie znaleziono pozycji w katalogu } Szukaj := -1; end; Zauważ, że istota przeszukującej tablicę pętli while skupia się w warunku, zaś jej treścią jest jedynie zwiększenie indeksu tablicy. Do sprawdzenia, czy tytuł zawiera dany ciąg znaków użyliśmy funkcji Pos, zwracającej pozycję, na której ciąg został znaleziony w zadanym łańcuchu (lub zero, jeśli go nie znaleziono). Przeszukiwanie kończy się w momencie, gdy indeks tablicy zrówna się z numerem ostatniej książki w katalogu.
Wyszukanie wszystkich książek o zadanym tytule (i wyświetlenie informacji o nich) jest już proste:
procedure SprawdzKsiazki(Tytul : string); { wyprowadza dane książek o zadanym tytule } var i : integer; Tytul : string; begin write('Podaj tytul szukanej ksiazki: '); readln(Tytul); i := 1; repeat i := Szukaj(Tytul, i); { znajdź kolejne } { wystąpienie tytułu } if i <> -1 then { znaleziono książkę } begin writeln('Pozycja katalogu nr ', i,':'); WypiszDane(Katalog[i]); Inc(i); { przejdź do następnej pozycji } end; until i = -1 { nie znaleziono więcej książek } end; Powyższa procedura cyklicznie wywołuje funkcję Szukaj, nakazując jej za każdym razem rozpoczęcie przeszukiwania od ostatnio znalezionej książki (dlaczego?) i wypisując dane znalezionych pozycji za pomocą procedury WypiszDane. W tym momencie łatwo już ustalić, czy książka została wypożyczona, a jeśli tak - to komu (w ramach ćwiczeń sugeruję Ci napisanie podobnych funkcji, umożliwiających ustalenie, które książki zostały wypożyczone osobie o podanym nazwisku).
Właściwą obsługą wypożyczenia lub zwrotu zajmuje się przedstawiona poniżej procedura Wypozycz. Zaznaczenie książki jako wypożyczonej odbywa się przez wpisanie nazwiska wypożyczającego do odpowiedniego pola rekordu i zwiększenie licznika wypożyczeń. W chwili zwrotu pole Wypozyczajacy jest zerowane.
procedure Wypozycz(var r : Ksiazka); { obsługuje wypożyczenie/zwrot książki } begin writeln; writeln('Biezace dane ksiazki:'); WypiszDane(r); { wypisz informacje o książce dla kontroli } writeln; writeln('Wprowadz nazwisko wypozyczajacego.'); writeln('W przypadku zwrotu ksiazki nacisnij Enter.'); with r do { wprowadź nazwisko lub wyczyść pole rekordu } begin readln(Wypozyczajacy); if Wypozyczajacy <> '' then Inc(LbPoz); { przy wypożyczeniu } { zwiększ licznik wypożyczeń } end; end; Powyższą procedurę można wywołać np. podczas sprawdzania danych książek, czyli z procedury SprawdzKsiazki. Wymaga to niewielkiej modyfikacji tej ostatniej:
{ ... } writeln('Pozycja katalogu nr ', i,':'); WypiszDane(Katalog[i]); writeln('Obsluga wypozyczen: nacisnij "W"'); writeln('Nastepna pozycja: nacisnij dowolny klawisz.'); if UpCase(ReadKey) = 'W' then Wypozycz(Katalog[i]); Inc(i); { przejdz do nastepnej pozycji } { ... } Jeśli podczas przeglądania wybranych pozycji bibliotekarz zechce wypożyczyć książkę lub przyjąć jej zwrot, powinien nacisnąć klawisz W, co wywoła procedurę Wypozycz. Obsługą naciśnięcia klawisza zajmuje się "importowana" z modułu Crt (zobacz str. 121) funkcja ReadKey, zaś funkcja UpCase uniezależnia program od wielkości liter (w lub W).
Statystykę wypożyczeń obsługuje dość obszerna, chociaż bardzo prosta procedura Statystyka. Oblicza ona najmniejszą, największą i średnią wartość licznika wypożyczeń dla całego katalogu. Wykorzystana w niej metoda wyszukiwania najmniejszej i największej wartości w tablicy jest bardzo typowa i szeroko stosowana, a polega na badaniu, czy kolejny element tablicy nie jest mniejszy (większy) od aktualnego minimum (maksimum); jeśli tak, jest on przyjmowany jako nowe minimum (maksimum).
procedure Statystyka; var i : integer; { licznik pętli } min, max, sum : longint; { minimum, maksimum i suma } poz_min, poz_max : integer; { pozycje minimum i maksimum } begin min := 1000000; { inicjalizuj minimum, maksimum i sumę } max := 0; sum := 0; for i := 1 to LbPoz do with Katalog[i] do begin if Licznik > max then { znaleziono nowe maksimum } begin max := Licznik; { zapamiętaj nowe maksimum } poz_max := i; { i numer odpowiedniej pozycji } end; if Licznik < min then { to samo dla minimum } begin min := Licznik; poz_min := i; end; sum := sum + Licznik; { zwiększ licznik wypozyczeń } end; writeln('Statystyka wypozyczen:'); { wypisz wartości } { statystyk } writeln('Srednia wypozyczen na jedna ksiazke: ', sum/Licznik:6:0); writeln('Ksiazka najczesciej wypozyczana (', max, ' wypozyczen):'); WypiszDane(Katalog[poz_max]); writeln('Ksiazka najrzadziej wypozyczana (', min, ' wypozyczen):'); WypiszDane(Katalog[poz_min]); end; Pozostała nam do rozwiązania sprawa alfabetycznego uporządkowania zawartości katalogu. Zadanie to, będące jednym z klasycznych zadań programowania, nosi nazwę sortowania. Liczba znanych obecnie algorytmów sortowania jest dość spora; my zajmiemy się jednym z najprostszych - tak zwanym sortowaniem przez proste wybieranie (ang. selection sort), które, chociaż niezbyt efektywne, jest łatwe do zrozumienia i interpretacji.
Załóżmy, że musimy uporządkować (od wartości najmniejszej do największej) tablicę zawierającą pięć liczb całkowitych. W pierwszym kroku wyszukujemy najmniejszą liczbę w tablicy i zamieniamy ją miejscami z liczbą znajdującą się na pierwszej pozycji. W tym momencie najmniejsza wartość w tablicy jest już zlokalizowana i ustawiona na właściwym miejscu, "odcinamy" więc pierwszą pozycję tablicy i powtarzamy wyszukiwanie minimum dla pozostałych pozycji (od 2 do 5). Po znalezieniu minimum zamieniamy je miejscami z liczbą na pozycji drugiej, czego efektem jest ustalenie dwóch pierwszych pozycji uporządkowanej tabeli. Po "odcięciu" pierwszych dwóch pozycji (już uporządkowanych) powtarzamy sortowanie dla pozycji 3 do 5 i tak dalej, aż do chwili, kiedy pozostanie nam do przeszukania ostatnia pozycja tablicy (oczywiście zawierająca największą liczbę, "zepchniętą" na sam koniec). A oto przykład:
Przed sortowaniem
(krok 0) W trakcie sortowania krok 1 krok 2 krok 3 krok 4 4 1 (4) 1 1 1 1 4 (1) 2 (4) 2 2 3 3 3 3 (3) 3 5 5 5 5 4 (5) 2 2 4 (2) 4 5 (4)Kolorem czarnym zaznaczono zakres przeszukiwania tablicy (jak widać, w kolejnych krokach zmniejsza się on o 1), zaś w nawiasach umieszczono wartości, które znajdowały się na danej pozycji przed zamianą, tj. 4 (1) oznacza, że na danej pozycji znajdowała się liczba 1, zamieniona następnie z liczbą 4.
Jak widać, nasz algorytm jest całkiem prosty, choć okupione jest to efektywnością (bardziej ambitnym Czytelnikom polecam przestudiowanie innych algorytmów sortowania, z których najczęściej stosowanym i chyba najefektywniejszym jest tzw. szybkie sortowanie (ang. quicksort), zilustrowane m.in. dostarczanym wraz z Turbo Pascalem programem QSORT.PAS). Niestety, sprawę komplikuje nieco wymaganie sortowania według tytułów lub nazwisk autorów. Jak już się zapewne zorientowałeś, wykorzystywane w algorytmie wyszukiwanie najmniejszej wartości polega na porównywaniu zawartości odpowiedniej komórki tablicy z bieżącą wartością minimum. Aby posortować katalog według tytułów, należy porównywać pola Tytul odpowiednich rekordów; sortowanie według nazwisk autorów wymaga porównywania pól Autor. Czyżby więc należało napisać dwie oddzielne procedury sortujące? Na szczęście nie, chociaż bardzo wielu początkujących programistów radzi sobie właśnie w ten sposób (czego absolutnie nie należy polecać!). Zauważ, że w obu przypadkach mechanizm sortowania jest identyczny, zaś różnica sprowadza się wyłącznie do porównywania różnych pól rekordów. Wystarczy zatem napisać odpowiednie funkcje porównujące dwa rekordy według pól Autor i Tytul, a następnie wywoływać je w zależności od aktualnego kryterium sortowania. Zwyczajowo funkcje takie zwracają wartość -1, 0 lub 1 odpowiednio do tego, czy pierwszy z porównywanych obiektów jest mniejszy, równy lub większy od drugiego. W naszym przypadku - ponieważ sortujemy obiekty rosnąco - wystarczy informacja, czy pierwszy rekord jest mniejszy od drugiego, czy też nie. Oto przykład funkcji porównującej rekordy według zawartości pola Tytul (drugą funkcję napisz sam):
function PorownajTytul(r1, r2 : Ksiazka) : integer; { porównuje rekordy według pola Tytul } begin if r1.Tytul < r2.Tytul then PorownajTytul := -1 else PorownajTytul := 1; end; Sama procedura sortująca powinna wykonywać algorytm opisany powyżej, wywołując odpowiednią funkcję porównującą w zależności od zadanego przez użytkownika kryterium. Na obecnym etapie znajomości Pascala jesteśmy w stanie zrealizować takie wywołanie w oparciu o instrukcję warunkową, jednak "prawdziwe" programy wykorzystują nieco inne rozwiązanie, oparte o tak zwane parametry proceduralne. Omawianie tego zagadnienia wykracza poza ramy niniejszej książki, jednak w skrócie metoda ta polega na przekazaniu procedurze sortującej wskaźnika opisującego położenie funkcji porównującej (jej adresu). Podstawiając pod parametr adresy różnych funkcji jesteśmy w stanie elegancko zmieniać kryteria sortowania.
A oto treść procedury:
procedure Sortuj(Kryterium : integer); { sortuje zawartość katalogu według zadanego kryterium } var i, j : integer; CzyWiekszy : integer; Pomoc : Ksiazka; begin for i := 1 to LbPoz-1 do { wykonaj LbPoz-1 przebiegów } for j := i to LbPoz do begin if Kryterium = 1 then { porównanie wg tytułu/autora } CzyWiekszy := PorownajTytul(Katalog[i], Katalog[j]) else CzyWiekszy := PorownajAutor(Katalog[i], Katalog[j]); if CzyWiekszy = 1 then { trzeba zamienić elementy } begin Pomoc := Katalog[i]; { zachowaj pierwszy rekord } Katalog[i] := Katalog[j]; { wstaw drugi } { do pierwszego } Katalog[j] := Pomoc; { wstaw zachowany } { do drugiego } end; end; end; Jak widać, procedura wykonuje cykliczne porównania i ewentualne zamiany w dwóch zagnieżdżonych pętlach, z których zewnętrzna wyznacza kolejne kroki algorytmu, zaś wewnętrzna obsługuje przeglądanie pozostałych jeszcze do uporządkowania rekordów. Sama zamiana elementów wykorzystuje typowy schemat "podpierający się" pomocniczym rekordem służącym do tymczasowego przechowania danych. Zauważ wreszcie, że aby posortować rekordy malejąco, wystarczy zmodyfikować warunek
if CzyWiekszy = 1
zastępując wartość 1 wartością -1.
Jak nietrudno się domyślić, operacja sortowania powinna poprzedzać wyprowadzenie zawartości katalogu na ekran lub drukarkę. Procedura kojarząca obydwie operacje może wyglądać następująco:
procedure WypiszKatalog; var Kryterium : integer; begin write('Sortowanie wedlug tytulow (1)', ' lub nazwisk autorow (2)'); readln(Kryterium); Sortuj(Kryterium-1); { parametr powinien być 0 lub 1 } Wypisz; { wypisz posortowany katalog } readln; { i pozwól go przeczytać } end; W ten sposób zakończyliśmy budowę zasadniczych klocków, z których składać się ma nasz program. Pozostaje jeszcze napisanie ładnego menu:
function Menu : integer; var c : char; begin ClrScr; { wyczyść ekran - wymaga włączenia modułu Crt } writeln('Program biblioteczny KATALOG, wersja 1.0, 1996'); writeln; writeln('Wybierz funkcje:'); writeln('1: Wyszukiwanie pozycji wg tytulu', ' i obsluga wypozyczen'); writeln('2: Wyswietlenie wszystkich pozycji'); writeln('3: Dodanie ksiazki do katalogu'); writeln('5: Statystyka wypozyczen'); writeln('0: Koniec pracy'); writeln; writeln; repeat { czytaj klawiaturę, } c := ReadKey { również wymaga włączenia modułu Crt } until c in ['0'..'5']; { ignoruj pozostałe znaki } Menu := ord(c)-ord('0'); end; Sama część operacyjna programu jest również bardzo typowa. Wykorzystuje ona pętlę repeat, cyklicznie wyświetlającą menu dostępnych operacji. Kod zwrócony przez funkcję Menu jest wykorzystywany do wywołania odpowiedniej operacji w instrukcji case. Zwrócenie przez funkcję Menu wartości 0 powoduje zakończenie działania pętli, a tym samym całego programu.
begin { część operacyjna } repeat Wybor := Menu; { wybierz opcję z menu } case Wybor of 1 : SprawdzKsiazki; 2 : begin WypiszKatalog; readln; end; 3 : DodajKsiazke; 5 : Statystyka; end; until Wybor = 0; { opcja 0 = koniec pracy } end. { program Biblioteka } Jak zdążyłeś zapewne zauważyć, program nie umożliwia usunięcia książki z katalogu. Przeznaczona jest do tego brakująca czwarta pozycja menu, której zaprogramowanie proponuję Ci w ramach ćwiczeń.
Po poskładaniu opisanych w tym rozdziale fragmentów w jedną całość (najlepiej w kolejności, w jakiej były cytowane) uzyskujesz gotowy program. Niestety, pełnię szczęścia zakłócają dwa mankamenty, z których co najmniej jeden jest bardzo poważny. Okazuje się mianowicie, że nasz katalog istnieje dopóty, dopóki użytkownik nie wybierze opcji "Koniec pracy" - kolejne uruchomienie programu wiąże się z koniecznością wpisania wszystkich książek od nowa (co jest niedopuszczalne!!). No i sama pojemność katalogu też mogłaby być większa...
Rozwiązaniem naszych problemów zajmiemy się już za chwilę, w dwóch kolejnych rozdziałach traktujących o plikach oraz zmiennych dynamicznych.
Poprzedni | Spis treści | Następny | Wersja spakowana |