2
Wydano za zgodą Rektora
Materiały pomocnicze do zajęć z przedmiotu „Wstęp do programowania”
dla studentów kierunku informatyka.
W procesie wydawniczym pominięto
etap opracowania językowego.
Wersja elektroniczna materiałów
została przygotowana przez Autorów.
algorytm, program, instrukcja,
iteracja, zmienna plikowa, tablica, funkcja,
procedura, rekord, wskaźnik, stos, moduł, obiekt,
klasa, konstruktor, metoda, aplikacja
ISBN 978-83-7199-862-7
Oficyna Wydawnicza Politechniki Rzeszowskiej
al. Powstańców Warszawy 12, 35-959 Rzeszów
e-mail: oficyna1@prz.rzeszow.pl
3
SPIS TREŚCI
1.
Wprowadzenie do programowania ......................................... 7
1.1.
Kryteria wyboru języka programowania ............................. 7
1.2.
Efektywność języków niskiego, średniego i wysokiego
poziomu ................................................................................ 8
1.3.
Język Pascal w ujęciu historycznym .................................... 9
1.4.
Ś
rodowiska programistyczne używające języka Pascal .... 10
1.5.
Etapy tworzenia programu komputerowego ...................... 14
1.6.
Cechy dobrego programu komputerowego ........................ 14
1.7.
Kod źródłowy programu w kontekście języka
programowania .................................................................. 15
1.8.
Rola kompilatora w procesie powstawania programu ....... 17
1.9.
Architektura kompilatora ................................................... 18
1.10.
Podział kompilatorów ze względu na platformę ................ 19
1.11.
Podział kompilatorów ze względu na liczbę przejść ......... 20
2.
Algorytmy i schematy blokowe ............................................. 22
2.1.
Podstawowe cechy algorytmów ......................................... 22
2.2.
Klasyfikacja algorytmów ze względu na sposób
implementacji ..................................................................... 23
2.3.
Klasyfikacja algorytmów ze względu na sposób
zaprojektowania ................................................................. 24
2.4.
Właściwości schematów blokowych ................................. 26
2.5.
Schematy blokowe wybranych algorytmów ...................... 27
2.6.
Zadania ............................................................................... 34
3.
Podstawowe elementy języka Pascal ..................................... 36
3.1.
Zalety języka Pascal ........................................................... 36
3.2.
Standardowe symbole i słowa kluczowe ........................... 36
4
3.3.
Sposoby zapisu liczb .......................................................... 37
3.4.
Sposoby notowania komentarzy ........................................ 38
3.5.
Zasady deklarowania stałych i zmiennych ........................ 39
3.6.
Definicja typu i rodzaje typów danych .............................. 40
3.7.
Rodzaje operatorów ........................................................... 49
3.8.
Priorytet operatorów .......................................................... 54
3.9.
Struktura programu ............................................................ 54
4.
Instrukcje ................................................................................ 57
4.1.
Instrukcje proste ................................................................. 57
4.2.
Instrukcja złożona .............................................................. 58
4.3.
Instrukcja warunkowa ........................................................ 59
4.4.
Instrukcja wyboru .............................................................. 61
4.5.
Instrukcje iteracyjne ........................................................... 63
4.6.
Procedury Break i Continue ............................................... 70
4.7.
Pętle i tablice ...................................................................... 71
4.8.
Zadania ............................................................................... 74
5.
Procedury i funkcje ................................................................ 75
5.1.
Składnia procedury ............................................................ 75
5.2.
Przekazywanie parametrów do procedur i funkcji ............ 77
5.3.
Procedury i dyrektywa zapowiadająca Forward ............... 80
5.4.
Składnia funkcji ................................................................. 81
5.5.
Przeciążanie funkcji ........................................................... 85
5.6.
Wywołanie funkcji przez inną funkcję .............................. 86
5.7.
Zmienne globalne i lokalne ................................................ 87
5.8.
Funkcje rekurencyjne ......................................................... 90
5.9.
Typ funkcyjny .................................................................... 91
5.10.
Procedury kierujące działaniem programu ........................ 92
5.11.
Kryteria stosowania procedur i funkcji .............................. 94
5
5.12.
Moduły ............................................................................... 95
5.13.
Zadania ............................................................................... 97
6.
Operacje na plikach ............................................................... 98
6.1.
Rodzaje plików .................................................................. 98
6.2.
Etapy przetwarzania pliku .................................................. 99
6.3.
Przykładowe programy działające na plikach ................. 102
6.4.
Zadania ............................................................................. 104
7.
Wskaźniki .............................................................................. 106
7.1.
Definicja zmiennej wskaźnikowej ................................... 106
7.2.
Procedury dynamicznego rezerwowania i zwalniania
pamięci ............................................................................. 107
7.3.
Wskaźniki i tablice ........................................................... 111
7.4.
Operacje arytmetyczne na wskaźnikach .......................... 113
8.
Struktury dynamiczne.......................................................... 114
8.1.
Kolejka ............................................................................. 114
8.2.
Lista jednokierunkowa ..................................................... 114
8.3.
Stos jako dynamiczna struktura danych ........................... 117
8.4.
Zadania ............................................................................. 121
9.
Klasy ...................................................................................... 123
9.1.
Zasady deklarowania klasy .............................................. 123
9.2.
Istota programowania obiektowego ................................. 124
9.3.
Konstruktor i destruktor ................................................... 128
9.4.
Typy konstruktorów ......................................................... 130
9.5.
Dziedziczenie ................................................................... 133
9.6.
Polimorfizm ..................................................................... 134
9.7.
Metody wirtualne ............................................................. 136
9.8.
Metody dynamiczne ......................................................... 138
6
10.
Programowanie obiektowe - aplikacje ............................... 141
10.1.
Komponenty LCL ............................................................ 141
10.2.
Inspektor obiektów ........................................................... 144
10.3.
Okno komunikatów i okno Form ..................................... 145
10.4.
Edytor źródeł .................................................................... 145
10.5.
Obiekt Button1 klasy TButton ......................................... 147
10.6.
Aplikacja Stoper ............................................................... 150
10.7.
Aplikacja Kalkulator ........................................................ 154
10.8.
Aplikacja Równanie kwadratowe .................................... 156
10.9.
Rysowanie figur geometrycznych.................................... 161
10.10.
Tablice i komponent TListBox .................................... 166
10.11.
Okna dialogowe ........................................................... 170
10.12.
Tablice jednowymiarowe i komponent TStringGrid ... 173
10.13.
Tablice dwuwymiarowe i komponent TStringGrid ..... 176
10.14.
Zadania ......................................................................... 180
LITERATURA ........................................................................... 181
7
1.
Wprowadzenie do programowania
Programowanie komputerów (pisanie programów lub skryptów, kodowa-
nie) jest formą działalności twórczej człowieka i polega na pisaniu użytecznych,
możliwych do przechowywania i rozszerzania, zestawów instrukcji, które mogą
być interpretowane (wykonywane) przez system komputerowy (komputer).
Instrukcje, reprezentujące m.in. operacje arytmetyczne i logiczne, czynią pracę
umysłową użytkownika komputera prostszą i bardziej efektywną.
Programowanie komputerów to czynność polegająca na zapisaniu rozwią-
zania konkretnego problemu zazwyczaj w postaci tekstowej, w wybranym języ-
ku programowania, z zachowaniem syntaktyki (składni) oraz semantyki (znacze-
nia poszczególnych symboli oraz ich funkcji w programie) tego języka.
Komputery można programować w jednym z wielu języków programowa-
nia. Ze względu na poziom abstrakcji, dzieli się je na:
•
języki wysokiego poziomu, zbliżone do języka naturalnego,
•
języki niskiego poziomu, stosowane do pisania programów bezpośred-
nio w kodzie maszynowym.
Program, przechowywany w postaci instrukcji binarnych, może być wyko-
nywany przez komputer. Mówi się, że jest on w postaci kodu maszynowego.
Początkowo programy były pisane wyłącznie w kodzie maszynowym. Obecnie
programy pisze się w językach wysokiego poziomu: C, C++, Java, Perl, Pascal.
Program napisany w języku wysokiego poziomu jest tłumaczony na kod maszy-
nowy za pomocą specjalnego programu.
1.1.
Kryteria wyboru języka programowania
Najważniejszym czynnikiem, przy wyborze pierwszego języka do nauki
programowania, jest to, na ile użyteczny będzie ten język w praktyce, na przy-
kład w przemyśle IT albo w otwartym projekcie. Język C++ lub Java będzie do-
brym wyborem, jednak oprócz użyteczności i popularności są ważne również
inne czynniki, na które trzeba zwrócić uwagę.
Wiele języków programowania jest projektowanych specjalnie po to, by by-
ły łatwe do nauki, szczególnie dla osób zaczynających swoją przygodę z pro-
gramowaniem komputerów. Języki Basic oraz Pascal należą do tej grupy języ-
ków. Są one jednak krytykowane za obniżoną wydajność, spowodowaną ograni-
czoną liczbą udogodnień. Należy pamiętać, że podstawową funkcją tych języ-
ków jest nauka programowania strukturalnego. Dla zaawansowanych aplikacji
mogą być potrzebne bardziej wyspecjalizowane i kompleksowe języki.
8
Niektóre języki programowania są odpowiednie dla początkujących, po-
nieważ mają prostą składnię (np. Python), natomiast dydaktycznie mogą spraw-
dzać się języki inne (np. Ada oraz Ruby).
Informacje o aktualnej popularności języków programowania (ranking ję-
zyków) można znaleźć na stronie http://www.tiobe.com/index.php/content/pa-
perinfo/tpci/index.html. W maju 2013 r. najbardziej popularne były języki:
C (18,729 %), Java (16,914 %), Objective-C (10,428 %), C++ (9,198 %), C#
(6,119 %), PHP (5,784 %), (Visual) Basic (4,656 %), Python (4,322 %), Perl
(2,276 %), Ruby (1,670 %), JavaScript (1,536 %), Visual Basic .NET (1,131 %),
Lisp (0,894 %), Transact-SQL (0,819 %), Pascal (0,805 %), Bash (0,792 %),
Delphi/Object Pascal (0,731 %), PL/SQL (0,708 %), Assembly (0,638 %), Lua
(0,632 %). W nawiasach podano w procentach liczbę programistów używają-
cych danego języka.
Nauka akademickich języków, takich jak Prolog czy Smalltalk, może wy-
dawać się nieprzydatna w przyszłej pracy zawodowej. Prostota tych języków
zmusza jednak do zmiany sposobu myślenia o programowaniu i uczy pewnych
umiejętności, które można potem przenieść na inne języki. Ponadto, znajomość
wielu języków programowania rozwija elastyczność w myśleniu i sprzyja roz-
wojowi intelektualnemu programisty.
1.2.
Efektywność języków niskiego, średniego i wysokiego poziomu
Jedne języki programowania oferują dużą szybkość i sprawność działania
programu, natomiast inne są łatwiejsze składniowo. Pisanie w kodzie maszyno-
wym, najczęściej w asemblerze, może dać w efekcie najszybszy i najbardziej
spójny program, jak to tylko możliwe. Jest to jednak zajęcie żmudne i czaso-
chłonne, gdyż asemblery mają bardzo precyzyjną składnię. Pojedyncza instruk-
cja asemblera jest tłumaczona zazwyczaj na jedną lub dwie instrukcje procesora.
Pojedynczej instrukcji języka wysokiego poziomu odpowiada kilka, kilka-
naście, a czasem nawet kilkadziesiąt instrukcji asemblera, co znacznie przyspie-
sza pisanie programu. Programista musi w tej sytuacji polegać na kompilatorze
i ufać, że przekształci on tę instrukcję w najefektywniejszą sekwencję elemen-
tarnych instrukcji procesora. Eksperymentując z opcjami kompilatora, można
uzyskać kod maszynowy szybszy, ale o większym rozmiarze albo wolniejszy,
ale o mniejszym rozmiarze
−
w zależności od potrzeby.
Z reguły, choć nie zawsze, im łatwiej zaprogramować jakieś zadanie, tym
wolniej będzie wykonywał się program realizujący to zadanie. Z tego wynika, że
najefektywniejsze – pod względem jakości programu i czasu poświęconego na
programowanie – są języki średniego poziomu. Na przykład, programy napisane
9
w asemblerze mogą działać szybciej niż programy napisane z wykorzystaniem
języków średniego poziomu. Z kolei nowocześnie zoptymalizowane kompilato-
ry dają często lepszy rezultat niż ten, jaki byłby w stanie osiągnąć przeciętny
programista.
W wielu przypadkach pisze się programy w językach niższego poziomu,
szczególnie te, które są składnikami systemów operacyjnych. Na przykład Li-
nuks i oprogramowanie GNOME zostały napisane w C, ponieważ w obu przy-
padkach niezwykle istotna była szybkość działania oprogramowania. Jądro sys-
temu Linuks nie może działać zbyt wolno, gdyż musi pracować równocześnie
z wieloma programami, nieraz bardzo wymagającymi, zaś oprogramowanie baz
danych przetwarza ogromne ilości danych, i powolne jego działanie mogłoby
spowodować niedrożność systemu. Oprócz tego, programy napisane w językach
niższego poziomu mają, z reguły, mniejszy rozmiar i zajmują mniej pamięci
operacyjnej, co w wypadku jądra systemu operacyjnego jest o tyle istotne,
ż
e pozostaje więcej zasobów dla innych programów, które dzięki temu działają
efektywniej.
1.3.
Język Pascal w ujęciu historycznym
Jeszcze kilkanaście lat temu język Pascal był jednym z najpopularniejszych
języków programowania. Jest to język uniwersalny, wysokiego poziomu, ogól-
nego zastosowania, oparty na języku Algol (ang. Algorithmic Language). Pascal
został opracowany przez Niklausa Wirtha w 1970 roku. Pierwotnie służył celom
edukacyjnym
−
głównie do nauki programowania strukturalnego.
Popularność Pascala w Polsce była większa niż w innych krajach ze
względu na:
•
dostępność kompilatorów w pirackich wersjach (zanim pojawiło się
prawo ochrony własności intelektualnej),
•
prostotę języka,
•
popularyzację przez wyższe uczelnie.
Szczyt popularności tego języka przypadł na lata 80. i początek 90. XX
wieku. Wraz ze zniesieniem ograniczeń narzuconych przez COCOM (Komitet
Koordynacyjny Wielostronnej Kontroli Eksportu), upowszechnieniem się sieci
Internet oraz systemów Uniks i Linuks
−
język Pascal został stopniowo wyparty
przez C i C++. Jedną z popularniejszych implementacji kompilatorów tego języ-
ka był produkt firmy Borland International o nazwie Turbo Pascal. W chwili
obecnej dość mocno rozpowszechnionym obiektowym dialektem języka Pascal
jest Object Pascal, który stanowi podstawę dla języków: Delphi, Delphi.NET
10
i Oxygene. Poza tym, istnieją wolne kompilatory Pascala (np. Free Pascal) oraz
wolne zintegrowane środowiska programistyczne (np. Lazarus).
Język Pascal bardzo rygorystycznie podchodzi do kontroli typów, tzn.
sprawdza, czy do zmiennej typu
A
nie próbuje się przypisać wartości typu
B
.
Jest on zatem językiem silnie typowanym.
Popularność Pascala wzrosła z pojawieniem się środowiska programistycz-
nego Delphi, opartego na obiektowym Pascalu, pozwalającego na błyskawiczne
tworzenie atrakcyjnych wizualnie aplikacji dla systemu operacyjnego Windows.
Wraz z pojawieniem się biblioteki Windows dla C++ oraz narzędzi do automa-
tycznego tworzenia graficznego interfejsu użytkownika (ang. Graphical User In-
terface = GUI), Pascal znów stracił na znaczeniu.
Dla pewnej grupy programistów niektóre cechy Pascala wykluczają jego
zastosowanie w poważnych projektach i są powodem krytyki tego języka. We-
dług nich Pascal powinien zostać jedynie narzędziem do nauki programowania.
W sprzeczności z tymi zarzutami stoi fakt, że w latach 80. i 90. XX wieku w ję-
zyku tym powstało tysiące aplikacji (również komercyjnych).
Zalety Pascala (czytelność kodu oraz rygorystyczna kontrola typów da-
nych) wraz z pojawieniem się języka C, stały się dla programistów wadami.
Promowanym przez twórców C standardem stała się natomiast zwięzłość kodu.
1.4.
Środowiska programistyczne używające języka Pascal
Object Pascal jest to obiektowy język programowania, stanowiący obiek-
towe rozszerzenie strukturalnego Pascala. Object Pascal jest używany jako ję-
zyk programowania w środowiskach programistycznych Borland Delphi i Kylix.
Object Pascal jest kontynuacją języka Turbo Pascal. Obecnie język ten właści-
wie jest nazywany Delphi Pascal. Charakteryzuje się głównie prostą składnią
i bardzo wysoką efektywnością tworzenia oprogramowania. Wpływ na to ma,
przede wszystkim, bardzo szybki kompilator (kilkakrotnie szybszy, w porówna-
niu do innych języków).
Free Pascal (FPK Pascal albo FPC) jest 32- oraz 64-bitowym kompilato-
rem języka Pascal, dostępnym na wiele różnych platform sprzętowych i syste-
mów operacyjnych.
Kompilator Free Pascal jest rozpowszechniany zgodnie z licencją GPL.
Biblioteki wykonawcze oraz dodatkowe pakiety, rozpowszechniane razem z
kompilatorem, objęte są jednak zmodyfikowaną licencją LGPL.
11
Wizualną częścią bibliotek dla FPC, zgodną z VCL (znanym z Delphi),
a także stworzeniem narzędzia typu RAD (ang. Rapid Application Development)
−
szybkiego tworzenia aplikacji, zajmuje się osobny projekt
−
Lazarus.
RAD jest to idea i technologia polegająca na udostępnieniu programiście
dużych możliwości prototypowania oraz obszernego zestawu gotowych kompo-
nentów (np. zapewniających dostęp do bazy danych). Takie podejście umożliwia
uzyskanie szybkiego efektu już w pierwszych krokach programistycznych, jed-
nocześnie stanowi poważne zagrożenie dla projektów o większych rozmiarach,
ze względu na łatwość nieprzemyślanego modyfikowania. Narzędzia RAD są
rozwinięciem pomysłu IDE i doskonale nadają się do tworzenia prototypów.
Wygląd aplikacji projektuje się rozmieszczając kontrolki w obszarze okna pro-
jektowanego programu (np. metodą „przeciągnij i upuść”).
Przykłady narzędzi typu RAD:
•
Microsoft Visual Studio dla Microsoft Windows,
•
Delphi firmy Borland oraz Embarcadero,
•
Lazarus i Kylix dla GNU/Linuksa,
•
Eclipse i NetBeans stworzone dla Javy i posiadające możliwość rozsze-
rzenia, w celu obsługi innych języków,
•
Zend Studio dedykowane dla języka PHP.
FPC posiada własne IDE, stworzone w trybie tekstowym, jednak nie jest
już ono wspierane. FPC rozpoznaje i kompiluje kod Pascala zapisany w poniż-
szych dialektach tego języka:
•
FPC – domyślny tryb pracy kompilatora, charakteryzujący się możliwo-
ś
cią przeciążania funkcji i operatorów oraz zagnieżdżania komentarzy,
•
obiektowy FPC – dopuszcza użycie rozszerzeń Object Pascala,
•
Delphi – zgodność z Delphi w wersji 4,
•
GPC – zgodność z kompilatorem GPC z pakietu GCC,
•
TP – zgodność z dialektem użytym w kompilatorze Turbo Pascal 7,
•
GCC (ang. GNU Compiler Collection) to zestaw kompilatorów tworzo-
ny w ramach projektu GNU. Początkowo skrótowiec GCC oznaczał
GNU C Compiler, ponieważ był to kompilator tylko języka C. Dostępne
są kompilatory dla języków: C, C++, Objective-C, Fortran, Java (GCJ),
a także
−
eksperymentalnie
−
Ada, Pascal i Go. GCC działa na wielu ar-
chitekturach i systemach operacyjnych, a do operacji na plikach obiek-
towych używa pakietu binutils.
12
Delphi (Borland Delphi) to zintegrowane środowisko programistyczne typu
RAD, przeznaczone do pracy pod kontrolą Microsoft Windows, działające zgod-
nie z zasadą dwustronnej edycji. Językiem programowania (obiektowym), osa-
dzonym w Delphi, jest Object Pascal.
Programy tworzone w Delphi muszą zostać skompilowane do postaci kodu
binarnego przed pierwszym wykonaniem. Mimo to, niektóre komponenty dzia-
łają już w trakcie tworzenia projektu, umożliwiając na bieżąco śledzenie postę-
pów w pracy nad projektem.
Delphi zapamiętuje informacje o właściwościach obiektów i udostępnia je
programiście. Informacje te umożliwiają programiście zmianę ich wartości bez
potrzeby pisania kodu programu oraz są używane w trakcie działania programu.
Technika ta nosi nazwę RTTI (ang. Run Time Type Information).
Programy tworzone w Delphi pracują na zasadzie obsługi zdarzeń. Każde
polecenie (np. kliknięcie myszką) generuje zdarzenie, które, poprzez wewnętrz-
ne mechanizmy programu, jest przesyłane do odpowiedniego komponentu. Rola
programisty ogranicza się tylko do dołączenia odpowiedniego kodu, umożliwia-
jącego obsługę tego zdarzenia.
Cechy i funkcjonalności Delphi:
•
szerokie wspomaganie obsługi relacyjnych systemów bazodanowych
(biurkowych oraz SQL-owych),
•
szeroki zestaw gotowych do użycia komponentów,
•
dwustronna edycja
−
funkcja środowiska programistycznego odzwier-
ciedlająca w kodzie programu zmiany dokonywane na jego zasobach
(i czasem odwrotnie),
•
możliwość budowania wizualnej części aplikacji za pomocą techniki
„przeciągnij i upuść” (ang. drag and drop),
•
szybki i efektywny kompilator języka Object Pascal (do natywnego ko-
du maszynowego),
•
rozszerzalne środowisko,
•
dołączone różne narzędzia, uzupełniające środowisko Delphi.
Delphi cieszy się w Polsce stosunkowo dużą popularnością, w głównej mie-
rze ze względu na relatywną prostotę i powszechność różnego rodzaju poradni-
ków dla początkujących.
Embarcadero Delphi XE4 jest rozbudowanym rozwiązaniem programi-
stycznym do szybkiego tworzenia prawdziwie natywnych aplikacji dla kompute-
13
rów PC, tabletów oraz smartfonów. Natywne aplikacje dają większą kontrolę,
większe bezpieczeństwo oraz są lepiej dopasowane do oczekiwań użytkowni-
ków. Dzięki Delphi XE4 nie trzeba prowadzić jednocześnie wielu projektów,
aby tworzyć natywne aplikacje dla komputerów PC, tabletów i smartfonów, de-
dykowane różnym platformom (iOS, Microsoft Windows, Mac OS). W Delphi
XE4 można zrobić to w ramach pojedynczego projektu. Tworzone aplikacje
w pełni wykorzystują możliwości i wydajność docelowych urządzeń, są wolne
od języków skryptowych i dodatkowych maszyn wirtualnych.
Kylix to zintegrowane środowisko dla programistów, pracujące pod Linuk-
sem, wyprodukowane przez firmę Borland. Pozwalało na pisanie aplikacji w ję-
zyku Delphi (Object Pascal) i korzystanie z komponentów CLX. Od wersji 3.
(2003 r.) Kylix umożliwiało także pisanie aplikacji w C++. Aplikacje CLX są
kompatybilne, na poziomie źródeł, ze środowiskiem Delphi, dzięki czemu zosta-
ło ułatwione przenoszenie tych aplikacji do systemu Windows. Możliwe jest tak-
ż
e przenoszenie aplikacji napisanych w Delphi lub w C++ z Windows do Linuk-
sa, ale należy w tym celu skorzystać z komponentów zawartych w międzyplat-
formowej bibliotece programistycznej CLX. Niestety, projekt Kylix został zanie-
chany po wydaniu trzeciej wersji.
Lazarus to zintegrowane środowisko programistyczne, oparte na kompila-
torze Free Pascal. Jest to, wzorowane na Delphi, wizualne środowisko progra-
mistyczne oraz biblioteka LCL (ang. Lazarus Component Library), która jest
odpowiednikiem VCL (ang. Visual Component Library).
Program napisany w środowisku Lazarus można bez żadnych zmian skom-
pilować dla dowolnego obsługiwanego procesora, systemu operacyjnego i inter-
fejsu okienek. Lazarus jest zgodny z Delphi. Jest brakującą częścią układanki,
która pozwala na rozwijanie programów, podobnie jak w Delphi, na wszystkich
platformach obsługiwanych przez FPC.
W odróżnieniu od Javy, która stara się, aby raz napisana aplikacja działała
wszędzie, Lazarus i Free Pascal starają się, aby raz napisana aplikacja kompi-
lowała się wszędzie. Ponieważ dostępny jest dokładnie taki sam kompilator, nie
trzeba wprowadzać zmian, aby otrzymać taki sam produkt dla różnych platform.
Główne cechy Lazarusa:
•
program jest udostępniany na licencji GNU GPL, natomiast biblioteki
−
na zmodyfikowanej licencji LGPL (co oznacza możliwość wykorzysta-
nia Lazarusa w projektach o zamkniętym kodzie),
•
szybkie przejścia pomiędzy różnymi interfejsami i systemami zapewnia-
ją biblioteki The Interface
−
Widget,
14
•
Lazarus jest jednym z nielicznych narzędzi, umożliwiającym tworzenie
aplikacji dla urządzeń Pocket PC z Windows CE albo QT Extended.
Lazarus posiada:
•
kompilator języka Pascal (Free Pascal),
•
edytor kodu źródłowego,
•
metodologię RAD,
•
technikę wizualnego tworzenia okien programu (tzw. form),
•
bibliotekę RTL (ang. Run-Time Library), stanowiącą zestaw gotowych
komponentów,
•
możliwość generowania kodu dla wielu:
systemów operacyjnych (Win32, Win64, Linuks, Mac),
platform sprzętowych (Pentium, PowerPC, Mac).
1.5.
Etapy tworzenia programu komputerowego
Aby rozwiązać problem, za pomocą programu komputerowego, należy:
•
określić zadania, które program ma realizować, w celu rozwiązania po-
stawionego problemu, z wyróżnieniem informacji wejściowych i wyj-
ś
ciowych
−
zdefiniować problem,
•
opracować kroki postępowania prowadzące do otrzymania informacji
wyjściowej na podstawie informacji wejściowej, czyli wymyślić sposób
realizacji tych zadań
−
zaprojektować algorytm,
•
zapisać algorytm w wybranym języku programowania, według zasad
i symboliki narzuconej przez konkretny język programowania – zako-
dować program,
•
skompilować i uruchomić program
−
zbudować program,
•
sprawdzić, czy program działa poprawnie
−
przetestować program.
1.6.
Cechy dobrego programu komputerowego
Inżynieria oprogramowania koncentruje się na praktycznej stronie wytwa-
rzania oprogramowania i obejmuje różne aspekty jego produkcji, mianowicie:
analizę i określenie wymagań (specyfikację), projektowanie, wdrożenie (imple-
mentację), integrację poszczególnych składowych w jedną całość, modyfikację
i w końcu dostosowywanie gotowego programu do nowych celów (ewolucję).
Bez względu na sposób wytwarzania, końcowy program powinien posiadać
pewne fundamentalne właściwości:
15
•
niezawodność
−
dobrze zaimplementowane algorytmy, prawidłowe za-
rządzanie zasobami, np. przepełnieniem bufora, oraz brak błędów lo-
gicznych typu: dzielenie przez zero, zły warunek krańcowy pętli itp.
•
solidność
−
umiejętność radzenia sobie programu z nieprawidłowymi
lub uszkodzonymi danymi, a także z niedostępnością zasobów takich
jak: pamięć, usługi systemu operacyjnego, usługi sieciowe itp.
•
użyteczność (ergonomia)
−
łatwość, z jaką użytkownik może wykorzy-
stać program, również do nietypowych zastosowań. Użyteczność zależy
od tekstowych i graficznych elementów, które poprawiają przejrzystość,
intuicyjność, spójność i kompletność interfejsu użytkownika.
•
przenośność
−
różnorodność sprzętu komputerowego (platform) i sys-
temów operacyjnych, na których program może być uruchamiany.
•
łatwość konserwacji
−
łatwość, z jaką program może być modyfikowa-
ny, przez aktualnych lub przyszłych deweloperów, w celu: zrobienia
ulepszeń lub dostosowania go do własnych potrzeb, adaptacji do nowe-
go środowiska albo usunięcia błędów i luk bezpieczeństwa. Łatwość
konserwacji nie musi być bezpośrednio widoczna dla użytkownika, ale
może znacznie wpłynąć na losy programu w dłuższym okresie.
•
efektywność/wydajność
−
ilość zasobów komputera używanych przez
program (im mniej, tym lepiej): czas procesora, wielkość pamięci, czę-
stość odczytu dysku twardego, pasmo danych karty sieciowej, angażo-
wanie uwagi użytkownika, pozostawianie plików tymczasowych itp.
1.7.
Kod źródłowy programu w kontekście języka programowania
Opracowanie algorytmu jeszcze nie rozwiązuje problemu. Dopiero zako-
dowanie go w postaci programu, w wybranym języku, przybliża nas do celu
(rys. 1.1). Ponieważ algorytm wymyśla człowiek, a wykonuje komputer, to za-
chodzi konieczność przetłumaczenia kolejnych kroków algorytmu na postać od-
powiednią (zrozumiałą) do automatycznego wykonywania przez maszynę.
Rys. 1.1. Droga od problemu do programu go rozwiązującego
Język programowania jest to sztuczny język, zaprojektowany do wydawa-
nia rozkazów maszynie, głównie komputerowi. Jak już wspomniano w akapicie
drugim na str. 7, opis konkretnego języka programowania jest zwykle dzielony
na dwa składniki: syntaktykę (formę) oraz semantykę (znaczenie).
Algorytm
Kod źródłowy
Program
Problem
16
Większość języków programowania jest czysto tekstowa. Tekst programu
zawiera: wyrazy, liczby i znaki przestankowe. Istnieją też języki programo-
wania, które są w swej naturze bardziej graficzne, a do zdefiniowania programu
stosuje się w nich relacje między symbolami graficznymi.
Tworzenia programów, za pomocą języków programowania, przypomina
układanie prostych zdań w języku naturalnym, zawierających wyrażenia arytme-
tyczne znane w matematyce. Programy pisze się w specjalnym edytorze tekstu.
W ten sposób powstaje tekst źródłowy, zwany też kodem źródłowym programu.
Syntaktyka języka opisuje dopuszczalne kombinacje symboli, które tworzą
składniowo poprawny program. Znaczenie konkretnej kombinacji symboli roz-
poznawane jest przez semantykę. Nie wszystkie programy poprawne syntak-
tycznie są poprawne semantycznie. Z semantyką związany jest system typów,
który definiuje, jak język programowania klasyfikuje wartości i wyrażenia, jak
może nimi manipulować i w jaki sposób mogą one wchodzić w interakcje.
W statycznym typowaniu wszystkie wyrażenia mają przypisane typy na
etapie kompilacji, zanim program zostanie wykonany. Statycznie typowane ję-
zyki mogą być typowane jawnie albo wnioskująco. W pierwszym przypadku
programista musi zapisać typy w sposób jawny, w określonych miejscach w tek-
ś
cie programu (np. podczas deklaracji zmiennych). W drugim przypadku kompi-
lator wnioskuje typy wyrażeń i deklaracji w oparciu o kontekst, w którym wy-
stępują. Popularnymi językami, w których manifestuje się typy są: C++, C#, Ja-
va. Pełne wnioskowanie o typach występuje w mniej znanych językach progra-
mowania: Haskell i ML.
Dynamiczne typowanie, zwane też późnym, określa bezpieczeństwo ope-
racji w trakcie jej wykonywania, tzn. typy są przypisywane wartościom otrzy-
mywanym w trakcie działania programu, a nie tekstowym wyrażeniom. Dyna-
micznie typowane języki nie wymagają od programisty wyraźnego (precyzyjne-
go) podania typu wyrażenia. Dzięki temu pojedyncza zmienna może odnosić się
do wartości o różnych typach w różnych punktach wykonywania programu.
Z tego powodu błędy związane z typami nie mogą być wykrywane automatycz-
nie, aż do momentu, gdy odpowiedni fragment programu jest rzeczywiście wy-
konywany, potencjalnie czyniąc debugowanie bardziej złożonym. Dynamicznie
typowanymi językami są: Lisp, Perl, Python, JavaScript i Ruby.
Słabe typowanie dopuszcza traktowanie wartości jednego typu jako warto-
ś
ci innego typu. To może być okazjonalnie użyteczne, ale może też powodować
pewnego rodzaju błędy programu, niewykrywalne na etapie kompilacji, a nawet
podczas uruchamiania.
17
Silne typowanie nie ma wad typowania słabego. Próba wykonania operacji
na wartości o niewłaściwym typie generuje błąd już na etapie kompilacji. Silnie
typowane języki są często określane mianem bezpiecznych (ang. type-safe).
Większość języków programowania posiada powiązaną bibliotekę standar-
dową, która jest udostępniana wszystkim implementacjom tego języka. Biblio-
teka standardowa, zwana też rdzeniową, zawiera definicje powszechnie uży-
wanych algorytmów, struktur danych oraz mechanizmów wejścia-wyjścia. Bi-
blioteka rdzeniowa jest, przez jej użytkowników, często traktowana jako część
języka. Projektanci tej biblioteki mogą jednak traktować ją jako oddzielny byt.
Po napisaniu programu, tekst źródłowy poddawany jest kompilacji przez
program zwany kompilatorem. Następnie, w fazie łączenia, dokonywane jest po-
łączenie procedur bibliotecznych z tekstem programu. Zadanie to realizuje linker
lub konsolidator.
Efektem końcowym przetworzenia kodu źródłowego jest program wyniko-
wy, który może być zapisany na dysku komputera i później wielokrotnie uru-
chamiany za pośrednictwem systemu operacyjnego.
1.8.
Rola kompilatora w procesie powstawania programu
Kompilacja, w programowaniu, to przetłumaczenie wersji źródłowej pro-
gramu na język maszyny (procesora). Kompilacji wersji źródłowej programu
dokonuje kompilator danego języka programowania.
Kompilator jest programem komputerowym lub zestawem programów,
który tłumaczy kod źródłowy, napisany w języku programowania (w języku
ź
ródłowym), na inny język komputerowy
−
język docelowy, posiadający zwykle
formę binarną, zwaną kodem obiektowym. Powodem, dla którego tłumaczy się
kod źródłowy jest utworzenie wykonywalnego programu (rys. 1.2). Kod obiek-
towy, zwany też modułowym, jest sekwencję wyrażeń lub instrukcji w języku
kodu maszyny (w postaci zer i jedynek).
Rys. 1.2. Droga od kodu źródłowego do wykonywalnego programu
Tekst źródłowy
programu
Reprezentacja
pośrednia
Program
wykonywalny
kompilacja
konsolidacja
Biblioteki stan-
dardowe i inne
18
Kompilator wykonuje większość lub wszystkie następujące czynności:
analizę leksykalną, preprocessing, parsowanie, analizę semantyczną, generowa-
nie kodu, optymalizację kodu.
Nazwa kompilator, przede wszystkim, jest używana w odniesieniu do pro-
gramów, które tłumaczą kod źródłowy z języka programowania wysokiego po-
ziomu do języka niskiego poziomu (kodu asemblera lub kodu maszynowego).
Kompilatory umożliwiają tworzenie programów niezależnych od maszyny.
Błędne działanie programu, spowodowane nieprawidłową pracą kompilato-
ra, jest bardzo trudne do wykrycia, dlatego twórcy kompilatorów dokładają
wszelkich starań, aby zapewnić wysoką jakość tego oprogramowania.
1.9.
Architektura kompilatora
Każdy kompilator składa się z trzech głównych części:
•
front-end
−
sprawdza, czy program użytkownika jest napisany popraw-
nie pod względem składniowym i znaczeniowym. Na tym etapie rapor-
towane są błędy, jeżeli istnieją i wykonywana jest kontrola typów.
Front-end generuje pośrednią reprezentację (ang. Intermediate Repre-
sentation – IR) kodu źródłowego, która jest przetwarzana w następnym
etapie. Front-end zarządza tablicą symboli, tzn. strukturą danych, która
dla każdego symbolu występującego w kodzie źródłowym przechowuje
informacje o jego położeniu, typie i zasięgu. Leksykalna analiza dzieli
tekst źródłowy na małe kawałki (tokeny). Każdy token jest pojedynczym
elementem języka programowania, takim jak: słowo kluczowe, identyfi-
kator, nazwa symbolu itp. Składnia tokena jest zwykle językiem regu-
larnym, więc do rozpoznania tokena można wykorzystać automat skoń-
czony, skonstruowany z wyrażeń regularnych. Niektóre języki (np. C)
wymagają fazy preprocessingu, obsługującej podstawienia makr oraz
kompilację warunkową. Faza ta występuje przed analizą syntaktyczną i
semantyczną. Analiza syntaktyczna obejmuje parsowanie sekwencji to-
kenów w celu wyznaczenia syntaktycznej struktury programu. Ta faza
buduje drzewo parsowania, która zastępuje liniową sekwencję tokenów
strukturą drzewiastą, zgodnie z zasadami gramatyki formalnej, definiu-
jącej składnię języka programowania. Drzewo parsowania jest analizo-
wane i transformowane w późniejszych fazach kompilacji. W czasie
analizy semantycznej kompilator dodaje informację znaczeniową do
drzewa parsowania i buduje tablicę symboli. Również wtedy odbywa się
semantyczne sprawdzanie takich rzeczy jak: typy (na obecność błędów),
wiązanie obiektów (kojarzenie zmiennych i referencji do funkcji z ich
definicjami), definitywne przypisanie (wymaganie inicjalizacji wszyst-
19
kich zmiennych lokalnych przed ich użyciem). To sprawdzanie kończy
się odrzuceniem źródła programu lub wygenerowaniem ostrzeżeń.
•
middle-end
−
na tym etapie są dokonywane optymalizacje niezależne od
kodów: źródłowego i maszynowego. Kod pośredni jest transformowany
do równoważnej funkcjonalnie, ale szybszej lub mniejszej formy. Ty-
powe transformacje, którym poddawany jest kod IR, w celu optymaliza-
cji, to: usunięcie nieużywanego lub nieosiągalnego kodu, wykrycie i
propagacja wartości stałych, relokacja obliczeń do rzadziej wykonywa-
nych miejsc, specjalizacja obliczeń opartych na kontekście, wstawienie
kodu typu inline. Tak zoptymalizowany kod może być dzielony między
różnymi wersjami kompilatora, obsługującymi różne języki i procesory
docelowe. Z powodu dodatkowego czasu i dodatkowej przestrzeni, po-
trzebnych na analizę i optymalizację, niektóre kompilatory domyślnie
opuszczają ją. Użytkownik musi wtedy sam wyraźnie „powiedzieć”
kompilatorowi, które optymalizacje chce włączyć.
•
back-end
−
jest odpowiedzialny za tłumaczenie kodu pośredniego, uzy-
skanego w fazie middle-end, na kod asemblera. Na tym etapie odbywają
się analizy, transformacje i optymalizacje przeznaczone dla określonego
typu komputera
−
generowany jest kod dla konkretnego procesora i sys-
temu operacyjnego. Instrukcje docelowe asemblera są dobierane dla
każdej instrukcji kodu pośredniego. Niektóre zmienne programu są alo-
kowane w rejestrach procesora. Generowane są także dane ułatwiające
debugowanie. Back-end wykorzystuje architekturę procesora w celu
rozdziału pracy między jednostki wykonawcze procesora, wypełnia slo-
ty opóźniające itp.
Przedstawiony podział kompilatora na 3 części pozwala łączyć front-end-y
różnych języków z back-end-ami różnych CPU. Praktycznym przykładem takie-
go podejścia są: GNU Compiler Collection, LLVM oraz Amsterdam Compiler
Kit, które mają wiele front-end-ów, wspólną analizę oraz wiele back-end-ów.
1.10.
Podział kompilatorów ze względu na platformę
Jedna z klasyfikacji kompilatorów dzieli je ze względu na platformę,
na której będzie wykonywany kod przez dany kompilator wygenerowany. Tę
platformę nazywa się docelową. Natywny lub hostowany kompilator to taki,
którego wynik działania przeznaczony jest do uruchamiania na takim samym ty-
pie komputera i systemu operacyjnego, na jakim uruchamiano kompilator.
20
Jeżeli skompilowany program daje się uruchomić na komputerze z CPU
i z systemem operacyjnym innymi od tych, na których kompilator był urucha-
miany, to do kompilacji został użyty kompilator skrośny (ang. cross-compiler).
Efekt działania kompilatora skrośnego jest przeznaczony do uruchamiania
na innej platformie. Kompilatory skrośne są często stosowane do wytwarzania
oprogramowania dla systemów wbudowanych (ang. embedded systems), które
w zamierzeniu nie będą obsługiwać środowisk wytwarzania oprogramowania.
Efekt działania kompilatorów, które produkują kod dla wirtualnej maszyny
(ang. Virtual Machine = VM), może być lub nie być uruchamiany na tej samej
platformie, na której kompilator wyprodukował kod.
Języki wyższego poziomu zwykle pojawiają się
−
w założeniu
−
z pewnym
rodzajem translacji. Są one projektowane albo jako języki kompilowane, albo
jako interpretowane. Interpretacja niezupełnie zastępuje kompilację. Interpreta-
cja jedynie ukrywa kompilację przed użytkownikiem i czyni ją bardziej stop-
niowaną. Specyfikacje niektórych języków wyraźnie mówią, że ich implementa-
cje muszą posiadać możliwość kompilacji. Tak jest np. z Common Lisp, jednak
nie ma niczego szczególnego, tkwiącego w definicji Common Lisp, co by blo-
kowało go przed byciem interpretowanym. Nowoczesne trendy w kierunku
kompilacji typu just-in-time oraz interpretacji bajtkodu rozmywają tradycyjny
podział kompilatorów i interpretatorów na kategorie.
Kompilator, dla względnie prostego języka, napisany przez jedną osobę,
może mieć postać pojedynczego, monolitycznego kawałka oprogramowania.
Gdy język źródłowy jest obszerny i skomplikowany, i wymagany jest wynik o
wysokiej jakości, wówczas projekt kompilatora może być podzielony na kilka
niezależnych faz, tzn. na małe fragmenty, które mogą być przekazane do opra-
cowania różnym osobom. Dzięki temu łatwo zastąpić jakąś fazę jej ulepszoną
wersją lub wprowadzić zupełnie nową fazę na późniejszym etapie. Wszystkie,
nawet najmniejsze kompilatory, posiadają więcej niż dwie fazy. Czasem trudno
wskazać miejsce, w którym fazy front-end i back-end łączą się ze sobą.
1.11.
Podział kompilatorów ze względu na liczbę przejść
Podział kompilatorów, ze względu na liczbę przejść (ang. pass), ma swoje
podłoże w ograniczeniach sprzętu komputerowego. Kompilacja wymaga prze-
prowadzenia mnóstwa działań, a wczesne komputery nie miały wystarczająco
pamięci, aby pomieścić jeden program, który zrobiłby to wszystko. Z tego po-
wodu kompilatory były dzielone na mniejsze programy, które robiły przejścia na
kodzie źródłowym lub na jego reprezentacji, wykonując jedną analizę lub trans-
lację. Umiejętność kompilacji w jednym przejściu (ang. single pass) uważana
21
jest za zaletę, gdyż upraszcza pisanie kompilatora, a poza tym kompilatory tego
typu (ang. one-pass) wykonują kompilację szybciej niż kompilatory multi-pass.
Tak więc, z powodu ograniczonych zasobów pierwszych systemów komputero-
wych, wiele wczesnych języków programowania było projektowanych tak, aby
mogły być kompilowane w jednym przejściu (np. Pascal).
W pewnych przypadkach zaprojektowana cecha języka programowania
może wymagać więcej niż jednego przejścia nad kodem źródłowym. Wadą
kompilacji w jednym przejściu jest to, że uniemożliwia ona przeprowadzenie
wielu wyrafinowanych optymalizacji, potrzebnych do wygenerowania kodu o
wysokiej jakości. Czasami trudno dokładnie obliczyć, ile przejść wykonuje
kompilator optymalizujący. Na przykład, różne fazy optymalizacji mogą anali-
zować jedno wyrażenie wiele razy, a inne
−
tylko jeden raz. Typowy kompilator
o wielu przejściach zwraca ostatecznie kod maszynowy. Są jednak inne rodzaje
kompilatora multi-pass, mianowicie:
•
source-to-source
−
kompiluje język wysokiego poziomu do tego samego
lub innego języka wysokiego poziomu. Na przykład, kompilator do au-
tomatycznego zrównoleglania kodu może do kodu wysokiego poziomu
dopisywać wstawki w tym samym języku wysokiego poziomu, określa-
jące jak prowadzić obliczenia równoległe.
•
stage compiler
−
kompiluje kod źródłowy do kodu asemblera teoretycz-
nej maszyn (np. maszyny języka Prolog
−
Warren Abstract Machine).
Kompilatory bajtkodu dla języków Java i Pyton są odmianami stage
kompilatora.
•
just-in-time compiler
−
w przypadku tego kompilatora, aplikacje są do-
starczane w postaci bajtkodu, który następnie jest kompilowany do kodu
natywnej maszyny, tuż przed wykonaniem. Ten typ kompilatora używa-
ny jest m.in. przez systemy: Smalltalk, Java oraz Microsoft .NET Com-
mon Intermediate Language (CIL).
Skompilowany kod źródłowy, zazwyczaj zapisywany w pliku na dysku ja-
ko tzw. plik obiektowy
−
object file, jest już w języku maszynowym, ale musi
jeszcze zostać połączony z bibliotekami. Biblioteki zawierają dodatkowe in-
strukcje programu, napisane wcześniej przez tego samego programistę albo
przez kogoś innego, i oddzielnie skompilowane. Łączenie skompilowanego ko-
du źródłowego z bibliotekami statycznymi jest wykonywane przez program
zwany linkerem (ang. link
−
łączyć), na etapie linkowania, określanego też mia-
nem konsolidacji. W ten sposób otrzymujemy program gotowy do uruchomie-
nia (plik wykonywalny). Dodatkowo, podczas konsolidacji, do pliku wynikowe-
go mogą być dołączone odpowiednie nagłówki i informacje charakterystyczne
dla konkretnego formatu pliku wykonywalnego.
22
2.
Algorytmy i schematy blokowe
Napisanie programu musi być poprzedzone opracowaniem odpowiedniego
algorytmu, tzn. przepisu na rozwiązanie konkretnego problemu.
Algorytmy można zapisywać na różne sposoby:
•
w języku naturalnym,
•
w pseudokodzie (w języku zbliżonym do naturalnego),
•
w postaci schematu blokowego,
•
w postaci prezentacji multimedialnej,
•
w postaci instrukcji programu.
2.1.
Podstawowe cechy algorytmów
Rozważmy pewien zestaw danych, np. {a1, a2, a3} i zdefiniujmy na nim
pewną operację (np.
⊗
), która wygeneruje wyniki {x, y, z}. Przez algorytm bę-
dziemy w tym przypadku rozumieć zestaw czynności (obliczeń), które należy
wykonać, aby otrzymać dane wyjściowe z danych wejściowych.
Ogólnie, algorytm to zbiór reguł postępowania, mający na celu prze-
tworzenie informacji wejściowych w informacje wyjściowe. Informacje wej-
ś
ciowe zazwyczaj są nazywane danymi, a informacje wyjściowe
−
wynikami.
Algorytm musi posiadać skończoną liczbę reguł postępowania i może
zawierać tylko pewien skończony zbiór czynności (instrukcji).
Algorytm opracowuje się dla rozwiązywania problemów o powtarzalności
metod wnioskowania i dla różnych wejść. Oznacza to, że algorytm służy do
rozwiązywania problemów tej samej klasy. Wykorzystywane dane powinny być
sparametryzowane, tzn. nie powinno się używać wielkości stałych, ale pewnych
symboli, reprezentujących te dane, np.: a = 1, b = 2, x = a.
Algorytm projektuje się dla zadań, dla których istnieje rozwiązanie.
W przypadku gdy trudno jest udowodnić istnienie rozwiązania, należy określić
moment przerwania wykonywania zbioru reguł.
Algorytm powinien uwzględniać wszystkie możliwe sytuacje, jakie mo-
gą wystąpić podczas rozwiązywania zadania. Przykładowo, podczas rozwią-
zywania równania
0
2
=
+
+
c
x
b
x
a
należy przewidzieć również okoliczność,
kiedy nie posiada ono rzeczywistych pierwiastków. Ponadto, dla
0
=
a
nie jest
to równanie kwadratowe, gdyż wtedy degeneruje się ono do równania liniowe-
23
go. Poza tym, dla
,
0
=
a
0
=
b
i
0
≠
c
równanie jest sprzeczne, natomiast gdy
,
0
=
a
0
=
b
i
0
=
c
równanie posiada nieskończenie wiele rozwiązań.
Algorytm może być realizowany przez człowieka albo przez maszynę.
W przypadku komputera to procesor wykonuje operacje arytmetyczne, czyli:
dodawanie, odejmowanie, mnożenie i dzielenie. Budowa procesora determinuje,
w pewnym zakresie, zbiór operacji, które można zastosować podczas rozwiązy-
wania danego zadania. Przykładami algorytmów są:
•
przepis na przygotowanie jakiegoś dania,
•
instrukcja składania komputera lub samochodu,
•
sposób rozwiązywania równania kwadratowego,
•
metoda obliczania wyznacznika macierzy itp.
2.2.
Klasyfikacja algorytmów
ze względu na sposób implementacji
Istnieją różne kryteria podziału algorytmów na klasy. Ze względu na sposób
implementacji, algorytmy dzieli się na:
•
rekurencyjne (rekursywne) lub iteracyjne,
•
szeregowe, równoległe lub rozproszone,
•
deterministyczne lub niedeterministyczne,
•
dokładne lub przybliżone.
Algorytm rekurencyjny to taki, który wywołuje siebie (odnosi się do sie-
bie) do momentu, aż pewien warunek zostanie spełniony. Algorytm iteracyjny,
do rozwiązania problemu, używa powtarzalnych konstrukcji, takich jak pętle.
Algorytm ten kończy działanie, gdy pętla wykona się określoną liczbę razy albo
gdy spełniony zostanie warunek przerwania tej pętli. Każdy algorytm rekuren-
cyjny posiada, mniej lub bardziej skomplikowaną, równoważną wersję ite-
racyjną, i odwrotnie. Przekształcenie algorytmu rekurencyjnego, w odpowiada-
jący mu funkcjonalnie algorytm iteracyjny, nazywane jest derekursywacją. Cho-
ciaż algorytmy rekurencyjne bywają prostsze do zrozumienia, to są obciążone
dużą złożonością obliczeniową i pamięciową (kosztowna obsługa stosu wywo-
łań). Algorytmy iteracyjne są bliższe architekturze procesorów i dlatego wyko-
nują się szybciej oraz zużywają mniej pamięci operacyjnej.
Algorytmy są zazwyczaj dyskutowane przy założeniu, że komputer może
wykonać jedną instrukcję w danym momencie. Algorytm projektowany do dzia-
łania w taki sposób nosi nazwę algorytmu szeregowego. Algorytmy równole-
głe wykorzystują natomiast zaletę architektury niektórych komputerów, polega-
24
jącą na możliwości „pracowania” kilku procesorów równocześnie nad rozwiąza-
niem danego problemu. Algorytmy rozproszone wykorzystują wiele kompute-
rów połączonych w sieć. Algorytmy równoległe i rozproszone dzielą problem na
mniej lub bardziej symetryczne podproblemy, by następnie zebrać te częściowe
wyniki z powrotem w całość. Algorytmy te zużywają czas nie tylko na oblicze-
nia, ale również na komunikację między procesorami. Na przykład, algorytmy
sortowania można efektywnie zamieniać na równoległe, ale proces komunikacji
jest zbyt czasochłonny. Iteracyjne algorytmy, w większości przypadków, można
konwertować na równoległe. Niektóre algorytmy nie dają się zrównoleglić.
Algorytm deterministyczny rozwiązuje problem podając dokładnie okre-
ś
lony rezultat na każdym etapie. Działanie tego algorytmu jest całkowicie zde-
terminowane przez warunki początkowe. Kilkukrotne uruchomienie algorytmu
deterministycznego doprowadzi za każdym razem do takiego samego wyniku.
Algorytm niedeterministyczny rozwiązuje zadanie poprzez zgadywanie,
chociaż tę metodę prób i błędów można uczynić bardziej skuteczną, stosując od-
powiednie heurystyki, czyli oparte na doświadczeniu i intuicji techniki zgady-
wania, przewidywania itp. Podczas gdy wiele algorytmów zwraca dokładne
rozwiązanie, algorytmy niedeterministyczne szukają aproksymowanego rozwią-
zania, które jest bliskie dokładnemu. Proces aproksymacji może stosować strate-
gię deterministyczną albo losową. Algorytmy przybliżone mają praktyczne
znacznie podczas rozwiązywania wyjątkowo trudnych problemów.
2.3.
Klasyfikacja algorytmów
ze względu na sposób zaprojektowania
Ze względu na sposób zaprojektowania lub paradygmat można wyróżnić
następujące typy algorytmów:
•
„brutalnej siły” (ang. brute-force) lub wyczerpującego (gruntownego)
przeszukiwania (ang. exhaustive search),
•
„dziel i zwyciężaj” (ang. divide and conquer),
•
dynamicznego programowania (ang. dynamic programming),
•
zachłanny (ang. greedy method),
•
programowania liniowego (ang. linear programming),
•
przeszukiwania i przeliczania (ang. search and enumeration).
Algorytm typu „brutalnej siły” przegląda (próbuje) wszystkie możliwe
rozwiązania w celu znalezienia tego najlepszego.
Algorytm typu „dziel i zwyciężaj”, w sposób powtarzalny, redukuje dany
problem (zazwyczaj poprzez zastosowanie rekurencji) do jednej lub więcej, ale
25
mniejszych instancji tego samego problemu do momentu, aż te instancje są na
tyle małe, że można je łatwo rozwiązać.
Jeżeli problem zawiera optymalne substruktury oraz nakładające się pod-
problemy, wówczas warto zastosować podejście zwane dynamicznym progra-
mowaniem. Pozwala ono uniknąć ponownego obliczania rozwiązań częścio-
wych, wyznaczonych dotychczas. Problem zawiera optymalne substruktury wte-
dy, gdy optymalne rozwiązanie całego problemu może być skonstruowane
z optymalnych rozwiązań podproblemów. Problem wykazuje posiadanie nakła-
dających się podproblemów wtedy, gdy te same podproblemy są używane do
rozwiązania wielu różnych instancji danego problemu.
Algorytm zachłanny jest podobny do programowania liniowego, z tym
wyjątkiem, że rozwiązania podproblemów nie muszą być znane na każdym eta-
pie. Zamiast tego algorytm zachłanny wybiera takie rozwiązanie, które jest op-
tymalne na danym etapie. Algorytm zachłanny rozszerza rozwiązanie za pomocą
najlepszej przewidywanej decyzji na bieżącym etapie i najlepszej decyzji podję-
tej na poprzednim etapie. Takie postępowanie nie jest wyczerpujące (nie prze-
szukuje wszystkich sytuacji) i w wielu przypadkach nie daje prawidłowych (do-
kładnych) rozwiązań problemów. Algorytmami tego typu są np. algorytmy wy-
znaczania minimalnego drzewa rozpinającego: Prima, Kruskala oraz Sollina.
Kiedy do rozwiązania problemu stosuje się programowanie liniowe, wtedy
określa się pewne nierówności na danych wejściowych, a następnie próbuje się
wyznaczyć maksimum lub minimum pewnej funkcji liniowej, zdefiniowanej dla
danych wejściowych. Rodzajowym algorytmem programowania liniowego jest
metoda simpleks. Bardziej skomplikowaną odmianą programowania liniowego
jest programowanie całkowitoliczbowe, z którym mamy do czynienia wówczas,
gdy przestrzeń dopuszczalnych rozwiązań jest ograniczona do liczb całkowitych.
Algorytm przeszukujący stara się znaleźć element o określonych właści-
wościach w zbiorze innych elementów. Do wirtualnych przestrzeni wyszukiwa-
nia stosuje się metody: przeszukiwania lokalnego, drzewa przeszukiwań oraz
drzewa gry. Metaheurystyczne metody przeszukiwania lokalnego to: symulowa-
ne wyżarzanie, przeszukiwanie tabu oraz programowanie genetyczne. Do metod
drzewa przeszukiwań zalicza się algorytmy: przeszukiwania wgłąb (ang. depth-
first search = DFS), przeszukiwania wszerz (ang. breadth-first search = BFS),
różne techniki przycinania drzewa, przeszukiwania z powrotami (ang. back-
tracking), a także technikę podziału i ograniczeń BB (ang. branch and bound).
Do przeszukiwania drzew gier, takich jak szachy czy warcaby, stosuje się algo-
rytmy minimax albo alpha-beta, w różnych odmianach.
26
Każda dziedzina nauki ma swoje problemy do rozwiązania i wymaga efek-
tywnych algorytmów. Podobne zagadnienia z jednej dziedziny są często dysku-
towane wspólnie. Algorytmy z różnych dziedzin niejednokrotnie przenikają się
i są ze sobą powiązane. Ze względu na obszar zastosowań, algorytmy dzieli się
m.in. na: algorytmy przeszukujące, sortujące, numeryczne, grafowe, kombinato-
ryczne, działające na łańcuchach, kryptograficzne, związane z kompresją da-
nych, sztuczną inteligencją i uczeniem maszynowym.
Algorytmy są także klasyfikowane ze względu na czas potrzebny do ich za-
kończenia w funkcji rozmiaru danych wejściowych. Wyróżniamy tutaj algoryt-
my o złożoności liniowej, kwadratowej, logarytmicznej, a nawet wykładniczej.
Złożoność czasowa algorytmu jest miarą ilości czasu potrzebnego do wy-
konania algorytmu w funkcji rozmiaru danych wejściowych (w funkcji długości
ciągu danych wejściowych). Złożoność czasowa jest zwykle określana poprzez
obliczenie liczby elementarnych operacji wykonywanych przez algorytm. Po-
nieważ potrzeba stałego czasu na wykonanie każdej elementarnej operacji, to
czas wykonania algorytmu i liczba elementarnych operacji są do siebie wprost
proporcjonalne, tzn. różnią się o co najwyżej stały współczynnik. Złożoność
czasową algorytmów wyraża się za pomocą notacji „duże O”, która opuszcza
składniki niższego rzędu. Mówi się, że tak zdefiniowana złożoność czasowa jest
opisana asymptotycznie, tzn. gdy rozmiar danych wejściowych rośnie do nie-
skończoności.
2.4.
Właściwości schematów blokowych
Algorytm jest często przedstawiany za pomocą graficznej reprezentacji
z wykorzystaniem symboli graficznych. Ta reprezentacja nosi nazwę schematu
blokowego lub sieci działań.
Schemat blokowy jest poglądową formą graficznego przedstawienia algo-
rytmu. Tworzy się go korzystając ze ściśle określonego zbioru figur geome-
trycznych oraz stosując ustalone reguły ich łączenia. We wnętrzu bloków,
w umowny sposób, zapisuje się występujące w algorytmie operacje arytmetycz-
ne, logiczne, operacje wejścia i wyjścia oraz warunki, od których zależą decyzje,
co do kolejności wykonywania obliczeń.
Zaletą schematów blokowych jest to, że graficznie reprezentują algorytm
zarówno ze względu na typy występujących w nim działań, jak i na ich kolej-
ność. Każdy schemat blokowy musi być spójny, tzn. od bloku start do bloku
stop musi prowadzić przynajmniej jedna droga.
27
Tabela 2.1. Symbole stosowane w schematach blokowych
Nazwa bloku
Symbol
Znaczenie
STRZAŁKA
Określa kierunek (drogę) przepływu danych
lub kolejność wykonywania działań.
START
Od tego bloku rozpoczyna się wykonywanie
algorytmu. Występuje tylko raz w schemacie.
Wychodzi z niego tylko jedna strzałka.
STOP
Na tym bloku kończy się wykonywanie algo-
rytmu. Wchodzi do niego co najmniej jedna
strzałka. Najczęściej występuje tylko raz,
ale dla podniesienia czytelności schematu,
może być powtórzony.
BLOK WEJŚCIA
/ WYJŚCIA
W tym bloku umieszcza się operacje
wprowadzania (odczytu) danych
albo wyprowadzania (zapisu) wyników.
BLOK
PRZETWARZANIA
(WYKONAWCZY)
W tym bloku umieszcza się obliczenia lub
podstawienia. Blok może zawierać grupę
operacji, w efekcie których zmienia się war-
tość, postać lub miejsce zapisu danych.
BLOK
DECYZYJNY
(WARUNKOWY,
KIERUJĄCY)
W tym bloku umieszcza się warunek logiczny
(prosty lub złożony), decydujący o dalszej
drodze postępowania. Jeżeli jest spełniony,
wtedy są wykonywane operacje na „TAK”, w
przeciwnym wypadku – na „NIE”. Do zapisu
warunku logicznego należy używać symboli:
=,
≠
, >,
≥
, <,
≤
,
∧
(i),
∨
(lub).
PROCES
UPRZEDNIO
ZDEFINIOWANY
W tym bloku wpisuje się nazwę procesu
(podprogramu), który chcemy wykonać, zde-
finiowanego poza bieżącym programem.
PUNKT
KONCENTRACJI
Punkt koncentracji jest używany dla podnie-
sienia czytelności schematu. Oznacza miej-
sce, do którego wchodzi kilka strzałek,
i z którego wychodzi tylko jedna strzałka.
2.5.
Schematy blokowe wybranych algorytmów
W rozdziale tym przedstawiono schematy blokowe prostych algorytmów.
Szczegółową ich analizę pozostawiamy Czytelnikowi.
NIE
TAK
28
Przykład 1. Wyznaczanie największej spośród trzech liczb:
.
,
,
c
b
a
Rys. 2.1. Schemat blokowy algorytmu wyznaczania największej spośród liczb: a, b, c
Przykład 2. Rozwiązywanie równania liniowego
0
=
+
b
x
a
.
Rys. 2.2. Schemat blokowy algorytmu rozwiązywania równania liniowego
STOP
NIE
TAK
NIE
TAK
wczytaj a, b
a=0
b=0
START
x
←
−
b / a
niesko
ń
czenie
wiele rozwi
ą
za
ń
równanie
sprzeczne
wypisz x
STOP
NIE
TAK
NIE
NIE
TAK
TAK
wczytaj a, b, c
a>b
b>c
a>c
wypisz a
wypisz c
wypisz b
wypisz c
START
29
Przykład 3. Wyszukiwanie największego elementu w tablicy n-elementowej
A
.
Rys. 2.3. Schemat blokowy algorytmu wyznaczania największego elementu tablicy
A
Przykład 4. Obliczanie silni liczby naturalnej n.
Rys. 2.4. Schemat blokowy algorytmu obliczania silni liczby naturalnej n
STOP
wczytaj n
START
silnia
←
1
i
←
1
NIE
TAK
i < n
wypisz silnia
i
←
i + 1
silnia
←
silnia*i
TAK
NIE
wczytaj n,
A[1], …, A[n]
START
i
←
2
max
←
A[1]
TAK
NIE
i > n
wypisz max
STOP
max
←
A[i]
i
←
i + 1
A[i] > max
30
Przykład 5. Wyznaczanie średniej arytmetycznej n wczytywanych liczb.
Rys. 2.5. Schemat blokowy algorytmu wyznaczania średniej arytmetycznej n liczb
Przykład 6. Obliczanie NWD liczb x i y metodą z odejmowaniem.
Rys. 2.6. Schemat blokowy algorytmu obliczania NWD metodą z odejmowaniem
n
←
n
−
m
m
←
m
−
n
START
n
←
x, m
←
y
STOP
NIE
TAK
NIE
TAK
wczytaj x, y
n > m
wypisz NWD(x,y)
n = m
NWD(x,y)
←
n
wczytaj n
START
s
←
0
k
←
n
NIE
k > 0
wypisz s
STOP
s
←
s / n
s
←
s + a
wczytaj a
k
←
k
−
1
TAK
31
Przykład 7. Obliczanie NWD liczb x i y metodą dzielenia z resztą.
Rys. 2.7. Schemat blokowy algorytmu obliczania NWD metodą dzielenia z resztą
Przykład 8. Szybkie (z minimalną liczbą mnożeń) obliczanie a
n
.
Rys. 2.8. Schemat blokowy algorytmu szybkiego obliczania a
n
wczytaj a, n
k
←
n
x
←
a
START
wynik
←
1
NIE
TAK
k
≠
0
wypisz wynik
STOP
TAK
NIE
k
←
k
−
1
wynik
←
wynik
∗
x
k
←
k / 2
x
←
x
∗
x
k mod 2
≠
0
NIE
TAK
wczytaj x, y
y
≠
0
x
←
y
r
←
x mod y
y
←
r
STOP
START
NWD(x,y)
←
x
wypisz NWD(x,y)
32
Przykład 9. Testowanie złożoności liczby naturalnej.
Rys. 2.9. Schemat blokowy prostego algorytmu sprawdzającego, czy liczba jest złożona
wczytaj n
START
r
←
n mod k
k
←
2
TAK
NIE
TAK
NIE
r = 0
n – liczba zło
ż
ona
k
←
k + 1
STOP
n = k
n –
liczba pierwsza
STOP
33
Przykład 10. Rozwiązywanie równania kwadratowego
.
0
2
=
+
+
c
x
b
x
a
Rys. 2.10. Schemat blokowy algorytmu rozwiązywania równania kwadratowego
TAK
NIE
TAK
D
=
0
x1
←
−
b/2a
x1
←
(
−
b+
√D
)/2a
STOP
x2
←
(
−
b
−
√D
)/2a
wypisz x1, x2
D
←
b*b
−
4*a*c
NIE
START
NIE
TAK
wczytaj a, b, c
a
≠
0
Równanie
liniowe
bx+c = 0
D
≥
0
Brak pierwiastków
rzeczywistych
x2
←
x1
34
Przykład 11. Przybliżone obliczanie pierwiastka kwadratowego z liczby a
≥
0.
Rys. 2.11. Schemat blokowy algorytmu
przybliżonego obliczania pierwiastka kwadratowego z nieujemnej liczby a
2.6.
Zadania
Narysować schematy blokowe algorytmów rozwiązujących problemy:
Zad. 2.1. zamiany liczby naturalnej dziesiętnej na liczbę szesnastkową.
Zad. 2.2. zamiany liczby szesnastkowej na liczbę w systemie dziesiętnym.
Zad. 2.3. obliczania NWD tablicy liczb, tzn.
).
,
,
,
(
NWD
2
1
n
a
a
a
K
Zad. 2.4. obliczania najmniejszej wspólnej wielokrotności pary liczb m i n we-
dług zależności:
).
,
(
NWD
/
)
(
)
,
(
NWW
n
m
n
m
n
m
⋅
=
Zad. 2.5. skracania ułamka zwykłego.
TAK
wypisz x
STOP
NIE
x
←
a/4
x1
←
x
x
←
0.5(x1+a/x1)
NIE
TAK
wczytaj a
a
≥
0
eps
←
1e-9
a jest ujemne
START
|x-x1| < eps
35
Zad. 2.6. dodawania dwóch ułamków zwykłych poprzez sprowadzenie ich do
wspólnego mianownika, a następnie skrócenie powstałego ułamka.
Zad. 2.7. zamiany ułamka niewłaściwego na część całkowitą i ułamek właściwy.
Zad. 2.8. zamiany ułamka dziesiętnego na ułamek w systemie binarnym.
Zad. 2.9. zamiany ułamka okresowego na ułamek zwykły.
Zad. 2.10. obliczania pierwiastka n-tego stopnia z liczby a, tzn.
n
a
x
=
metodą
Newtona:
1
1
)
1
(
−
+
−
+
=
n
i
n
i
i
x
n
x
n
a
x
, gdzie
i
x
−
kolejne przybliżenie
n
a
,
a
x
=
0
, z dokładnością eps taką, że
eps
x
x
i
i
<
−
+
1
.
Zad. 2.11. obliczania
∑
=
+
+
−
≈
N
n
n
n
x
n
x
0
1
2
)!
1
2
(
)
1
(
sin
oraz
∑
=
−
≈
N
n
n
n
x
n
x
0
2
)!
2
(
)
1
(
cos
.
Argument x jest dowolną liczbą rzeczywistą, N
−
liczbą naturalną.
Zad. 2.12. obliczania
!
...
!
2
!
1
1
2
n
x
x
x
e
n
x
+
+
+
≈
dla podanego n i x.
Zad. 2.13. obliczania metodą Hornera wartości wielomianu
.
)
(
0
1
1
1
a
x
a
x
a
x
a
x
w
n
n
n
n
n
+
+
+
+
=
−
−
K
Na przykład dla
:
4
=
n
))).
(
(
(
4
3
2
1
0
0
1
2
2
3
3
4
4
xa
a
x
a
x
a
x
a
a
x
a
x
a
x
a
x
a
+
+
+
+
=
+
+
+
+
Dane wejściowe to: n,
n
a
a
a
,
,
,
1
0
K
oraz konkretne
.
0
x
x
=
Zad. 2.14. obliczania wartości pochodnej wielomianu
)
(
x
w
n
w punkcie
.
0
x
x
=
Zad. 2.15. transponowania macierzy kwadratowej.
Zad. 2.16. zamiany parami elementów macierzy kwadratowej, położonych sy-
metrycznie względem drugiej (nie głównej) przekątnej.
Zad. 2.17. obracania macierzy kwadratowej o 90, 180 i 270 stopni.
Zad. 2.18. wyznaczania w tablicy liczby najbliższej zeru, ale różnej od zera.
Zad. 2.19. sortowania „w miejscu” jednowymiarowej tablicy liczb.
Zad. 2.20. sprawdzania, czy dwa wyrazy o tej samej długości są anagramami.
Zad. 2.21. odwracania „w miejscu” łańcucha znakowego o długości n. Czy trze-
ba osobno rozważać n parzyste i n nieparzyste?
Zad. 2.22. dokładnego dodawania i odejmowania wielocyfrowych (do stu cyfr)
liczb naturalnych. Uwaga: liczby mogą posiadać różną ilość cyfr.
36
3.
Podstawowe elementy języka Pascal
3.1.
Zalety języka Pascal
Pascal jest językiem wysokiego poziomu. Oznacza to, że użytkownik piszą-
cy program w języku Pascal nie musi znać szczegółów wewnętrznej budowy
komputera. Pascal ma wbudowane precyzyjne mechanizmy kontroli struktur i
reguł gramatycznych, dzięki temu łatwa jest identyfikacja i poprawianie błędów.
Oprócz tego Pascal jest językiem:
•
algorytmicznym
−
pomyślanym tak, aby łatwo można w nim zapisywać
algorytmy opracowane przez użytkownika,
•
strukturalnym
−
poszczególne fragmenty algorytmu można zapisać w
postaci wyraźnie wyodrębnionych struktur językowych,
•
modularnym
−
program można budować z oddzielnych 'cegiełek' (mo-
dułów), wymienialnych bez naruszenia pozostałej części programu,
•
publikacyjnym
−
istnieje możliwość opublikowania oryginalnego, inte-
resującego algorytmu w notacji języka Pascal.
3.2.
Standardowe symbole i słowa kluczowe
Języki programowania służą do przedstawienia algorytmów w postaci, któ-
ra może być wykonana przez komputer. Jedną z wygodnych form komunikacji
człowieka z maszyną jest zapis tekstowy, dlatego program jest tekstem
. Ze
względu na ograniczenia praktyczne, tekst ten jest ciągiem znaków na jednym
poziomie, tzn. nie ma indeksowania stosowanego np. w matematyce czy fizyce.
Program pascalowy buduje się z elementarnych jednostek tekstowych języ-
ka nazywanych symbolami.
Symbol może być pojedynczym znakiem bądź cią-
giem znaków alfabetu języka.
W
skład alfabetu języka Pascal wchodzą następujące symbole:
•
26 małych i 26 dużych liter alfabetu łacińskiego oraz znak _,
•
cyfry: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
•
znak odstępu (spacja),
•
jednoznakowe symbole specjalne: +
−
* / = ^ < > ( ) [ ] { }. , : ; ’ # $ @,
•
dwuznakowe symbole specjalne:
operator przypisania :=
operatory relacji:
<> <= >=
oznaczenie zakresu ..
37
Słowem kluczowym nazywamy każdy z wyrazów języka angielskiego:
and, array, as, asm, begin, case, class, const, constructor, destructor,
div, do, downto, else, end, except, exports, file, finalization, finally, for,
function, goto, if, implementation, in, inherited, initialization, inline,
interface, is, label, library, mod, nil, not, object, of, or, packed,
procedure, program, property, raise, record, repeat, set, shl, shr, string,
then, threadvar, to, try, type, unit, until, uses, var, while, with, xor.
Słowa kluczowe są zastrzeżone, tzn. programista nie może przedefiniować
ich znaczenia (używając np. słowa kluczowego jako nazwy zmiennej w swoim
programie). Słowa kluczowe mają specjalne znaczenie
−
są nazwami operato-
rów, elementami instrukcji itp.
Identyfikatorem (nazwą) jest dowolnie długi ciąg liter, cyfr i znaków pod-
kreślenia, zaczynający się od litery lub znaku podkreślenia. Znaczące są jednak
tylko 63 pierwsze znaki. Identyfikatory służą do oznaczania programów, stałych,
typów, zmiennych, etykiet, pól w rekordach, funkcji, procedur, parametrów for-
malnych oraz pól i metod w obiektach. W identyfikatorach nie wolno używać
polskich liter z ogonkami. Oprócz tego: litery
a
i
A
są nierozróżnialne, litery
b
i
B
są nierozróżnialne itd.
Przykłady poprawnych nazw:
liczba_1, stala3, m, B_205, XXL.
Przykłady niepoprawnych nazw:
2tablica,
ś
cie
ż
ka, Suma-1, #5.
Pewne identyfikatory mają nazwy ustalone przez twórców Pascala. Użyt-
kownik może przedefiniować pierwotne znaczenie tych identyfikatorów w sekcji
deklaracji programu i używać ich w zdefiniowanym przez siebie znaczeniu.
Oznacza to utratę wbudowanych w języku ułatwień. Predefiniowane identyfika-
tory służą do oznaczania stałych, zmiennych, typów, funkcji i procedur. Zostały
one zgrupowane w modułach standardowych
System, SysUtils i Math. Aby
używać tych modułów, należy je zadeklarować na początku programu, poprze-
dzając ich nazwy słowem kluczowym
uses. Nazwy z modułu System są dostęp-
ne bez potrzeby deklaracji tego modułu.
3.3.
Sposoby zapisu liczb
Liczby w języku Pascal zapisuje się w sposób podobny do notacji matema-
tycznej. W przypadku liczb rzeczywistych, zamiast przecinka oddzielającego
część całkowitą od części ułamkowej, stosuje się kropkę.
Liczby całkowite w systemie dziesiętnym zapisuje się używając cyfr ze
zbioru {0, 1, …, 9} poprzedzonych znakiem + lub
−
. Znak + można pominąć.
Liczba może zaczynać się od jednej lub więcej cyfr 0. Między pierwszą cyfrą
38
liczby, a znakiem + lub
−
może być odstęp. Przykłady poprawnych liczb w sys-
temie dziesiętnym: 2013, +02013, + 92,
−
0008885.
Liczby całkowite w systemie szesnastkowym tworzy się z cyfr należących
do zbioru {0, 1, 2, …, 9, A, B, C, D, E, F}, poprzedzonych symbolem $ oraz
ewentualnym znakiem + lub
−
. Symbol $ musi występować bezpośrednio przed
pierwszą cyfrą. Przykłady poprawnych liczb, zapisanych w systemie szesnast-
kowym: $9802, +$8FA3C,
−
$3FFFFFF,
−
$0007FFFFFF.
Liczba rzeczywista zbudowana jest z mantysy, litery E (lub e) oraz cechy.
Mantysa jest liczbą dziesiętną, składającą się z części całkowitej, kropki i części
ułamkowej, i może być poprzedzona znakiem + lub
−
. Cecha jest liczbą całko-
witą poprzedzoną ewentualnie znakiem + lub
−
. Mantysa i cecha może zawierać
wyłącznie cyfry dziesiętne. Dozwolone jest opuszczenie części ułamkowej wraz
z poprzedzającą ją kropką albo litery E i cechy. Symbolicznie liczbę rzeczywistą
można zapisać tak: mantysaEcecha, czemu odpowiada wartość mantysa
10
cecha
.
Przykłady:
−
56.980,
−
4.6592e
−
103, 0.314E+1, 2013E+00, +89.9377, 686.2e23.
3.4.
Sposoby notowania komentarzy
Komentarzem nazywamy dowolny ciąg znaków, ograniczony z lewej stro-
ny nawiasem klamrowym otwierającym {, a z prawej strony nawiasem klamro-
wym zamykającym }. Taki komentarz może rozciągać się na wiele linii tekstu
programu. Nawias { nie musi być pierwszym znakiem w danej linii. Znakami
ograniczającymi komentarz mogą być także symbole podwójne (* i *). Jeżeli
bezpośrednio po znaku { lub (* występuje symbol $, to taki komentarz jest trak-
towany jak dyrektywa kompilatora. Komentarze można zagnieżdżać. Tekst roz-
poczynający się od znaków // jest traktowany jak komentarz, ale tylko do końca
bieżącej linii. Umieszczanie komentarzy w programie należy do dobrej praktyki
programistycznej. Przykłady komentarzy:
{ Obliczanie miejsc zerowych funkcji kwadratowej }
(* Zapisanie wyników do pliku *)
// Sprawdzenie, czy delta jest większa od zera.
{ Komentarz (* zagnieżdżony *) } albo { Komentarz { zagnieżdżony } }
{$mode objfpc}
←
dyrektywa kompilatora.
Separatorami są w programie pascalowym: średnik, przecinek, komentarz,
odstęp (spacja), przejście do następnego wiersza (enter) oraz wszystkie znaki
sterujące ASCII, tzn. znaki o kodach od 0 do 31. Separatory służą do rozdziela-
nia poszczególnych elementów programu. Dowolne dwa słowa kluczowe, iden-
tyfikatory oraz liczby muszą być rozdzielone co najmniej jednym separatorem.
39
3.5.
Zasady deklarowania stałych i zmiennych
Stałe, zwane też literałami, mają za zadanie zwiększenie czytelności kodu
ź
ródłowego. Każde wystąpienie identyfikatora stałej jest równoważne wystąpie-
niu przypisanego mu wyrażenia stałego. Wyrażenie stałe to takie, które może
być obliczone przez kompilator bez wykonywania programu. Stałe nie muszą
być liczbami. Wartości stałych nie zmieniają się przez cały czas działania pro-
gramu. Predefiniowane w module
System stałe to: False, True, MaxInt,
MaxLongint. Stałe definiujemy po słowie const.
Przykłady definicji stałych:
const
kierunek = 'informatyka';
liczba_pi = 3.1415926;
c = 299792458;
falsz = false;
sygnal = #$7; // znak o kodzie ASCII równym $7
Wyrażenie stałe jest to wyrażenie, które może być obliczone przez kompi-
lator. W wyrażeniach stałych dopuszczalne jest stosowanie następujących funk-
cji standardowych:
Abs, Addr, Chr, Hi, High, Length, Lo, Low, Odd, Ord,
Pred, Round, SizeOf, Succ, Swap, Trunc. Wyrażenia liczbowe stałe są typu
Longint albo Extended, w zależności od kontekstu. Można też tworzyć wyraże-
nia stałe typu łańcuchowego lub zbiorowego. Za pomocą słowa kluczowego
const wyrażeniu stałemu można nadać nazwę i stosować tę nazwę w programie,
zamiast wyrażenia.
Przykłady wyrażeń stałych:
const cena = '100' + Chr(36);
rozmiar = (1 shl 20);
Zmienną nazywamy daną, która może przyjmować różne wartości w ra-
mach typu przypisanego do tej zmiennej. Zmienna jest wielkością zmieniającą
swoją wartość w trakcie wykonywania programu. Zmienna powinna być zade-
klarowana przed pierwszym użyciem. Jeżeli zmienna ma w programie zasięg
globalny, to w momencie deklaracji jest jej przypisywana wartość zerowa. De-
klaracje zmiennych sygnalizują kompilatorowi konieczność zarezerwowania dla
nich odpowiedniej ilości pamięci. Deklaracja zmiennej ma postać:
var nazwa_zmiennej : typ_zmiennej;
gdzie:
•
var
−
słowo kluczowe oznaczające deklarację zmiennej,
•
nazwa_zmiennej
−
identyfikator zmiennej,
•
typ_zmiennej
−
zbiór wartości, jaki może przyjmować ta zmienna.
40
Zmienna przechowuje pewną informację o stanie maszyny cyfrowej w jed-
nej lub więcej komórkach pamięci operacyjnej (rys. 3.1). Nazwa zmiennej po-
winna sugerować rolę tej zmiennej w programie.
Rys. 3.1. Graficzna reprezentacja zmiennej
Przykłady deklaracji zmiennych:
var miesiac : Byte;
pole, obwod : Double;
znak : Char;
tytul, autor : string;
Podczas deklaracji, zmiennym można nadać wartości początkowe:
var miesiac : Byte = 5;
pole : Double = 100.0;
znak : Char = 'Z';
tytul : string = 'Pan Tadeusz';
Uwaga: deklaracja
pole, obwod : Double = 100.0;
jest niepoprawna,
ponieważ niewiadomo, której zmiennej należy przypisać wartość 100.0.
3.6.
Definicja typu i rodzaje typów danych
Typem nazywamy zbiór wartości zmiennej. Typy dzieli się na standardowe
(wbudowane) i niestandardowe. Typy standardowe są predefiniowane przez
twórców języka programowania, natomiast wszystkie typy wprowadzone przez
programistę muszą zostać przez niego opisane. Stosowanie w programie identy-
fikatorów typów zwiększa czytelność kodu źródłowego. Każda stała, zmienna,
wyrażenie lub wartość zwracana przez funkcję jest pewnego typu. Typ wyraże-
nia określa także zestaw dopuszczalnych operacji, które można na nim przepro-
wadzić. Definicja typu jest następująca:
type nazwa_typu = opis_typu;
•
type
−
słowo kluczowe oznaczające definicję typu,
•
nazwa_typu
−
identyfikator typu,
•
opis_typu
−
opis jednego z typów: wyliczeniowego, okrojonego, łańcu-
chowego, tablicowego, rekordowego, zbiorowego, plikowego, klasowe-
go, wskaźnikowego, proceduralnego albo identyfikator typu standardo-
wego lub zdefiniowanego wcześniej przez użytkownika.
Przykłady definicji typów:
identyfikator
delta
123
(byte)
zmienna
typ
warto
ść
41
type pora_roku = (Wiosna, Lato, Jesien, Zima);
liczba_rzymska = string;
temperatura = Real;
macierz = array [1..5, 1..5] of Integer;
Typy predefiniowane dzieli się, ze względu na postacie i zakresy, na:
•
podstawowe
−
takie same bez względu na implementację języka,
•
ogólne
−
zależne od procesora i systemu operacyjnego.
Do typów podstawowych zalicza się typy: proste, łańcuchowe, struktural-
ne, typy opisujące obiekty, wskaźnikowe, proceduralne i wariantowe.
Typy proste dzieli się na:
•
porządkowe: wyliczeniowe, całkowite, znakowe, logiczne, okrojone,
•
rzeczywiste:
Real, Single, Double, Extended, Comp, Currency.
Wszystkie typy proste składają się ze skończonego i uporządkowanego
zbioru wartości. Z każdą wartością typu porządkowego jest związana liczba po-
rządkowa (liczba całkowita) jednoznacznie określająca umiejscowienie tej war-
tości wśród wszystkich wartości danego typu.
Na podstawie typów prostych można tworzyć inne, bardziej rozbudowane
struktury danych, tzw.
typy strukturalne. Wprowadzono je w związku z dąże-
niem do możliwie szerokiego zakresu zastosowań języka Pascal.
Typami struk-
turalnymi są typy: tablicowe, rekordowe, zbiorowe i plikowe.
Typ wyliczeniowy stosuje się do zbiorów o niewielkiej liczbie elementów,
na których nie wykonuje się operacji arytmetycznych.
type identyfikator_typu = (lista_identyfikatorów);
W nawiasach ( ) podaje się listę różnych nazw, które tworzą zbiór wartości
tego typu, przy czym pierwszej nazwie odpowiada wartość 0, drugi
−
1, trzeciej
−
2 itd. Przykłady definicji typu wyliczeniowego:
type pory_dnia = (rano, poludnie, wieczor, noc);
moje_grupy = (L5, L7, L12, L13, L16, L18);
kolory = (czerwony, zielony, niebieski);
greckie = (alfa, beta, gamma, delta);
Wszystkie
typy całkowite są predefiniowane i są podzbiorami zbioru liczb
całkowitych (który w matematyce jest nieskończenie liczny). Wartość porząd-
kowa elementu dowolnego typu całkowitego jest równa wartości tego elementu.
Typami porządkowymi nie są jednak typy całkowite
QWord i Int64. Typy cał-
42
kowite języka Free Pascal zebrano w tabeli 3.1. Dla typów
Integer i Cardinal
podano liczbę bajtów dla 32 bitowej implementacji języka.
Tabela 3.1. Typy całkowite i ich zakresy
Typ
B
wartość minimalna
wartość maksymalna
Shortint
P
1
−
128
127
Smallint
P
2
−
32 768
32 767
Longint
P
4
−
2 147 483 648
2 147 483 647
Byte
P
1
0
255
Word
P
2
0
65 535
DWord,
LongWord
P
4
0
4 294 967 295
QWord
P
8
0
18 446 744 073 709 551 615
Int64
P
8
−
9 223 372 036 854 775 808
9 223 372 036 854 775 807
Integer
O
4
−
2 147 483 648
2 147 483 647
Cardinal
O
4
0
4 294 967 295
P
– typ podstawowy,
O
– typ ogólny (zależny od rodzaju procesora i sys. oper.),
B – liczba bajtów pamięci potrzebna do przechowywania zmiennej tego typu
Przykłady deklaracji zmiennych typu całkowitego:
var a, b, c : Shortint;
silnia : QWord;
licznik : Cardinal;
W programach zaleca się stosowanie ogólnych typów całkowitych, które
lepiej wykorzystują rodzaj procesora i zainstalowany system operacyjny.
W języku Free Pascal zdefiniowano dwa podstawowe
typy znakowe i je-
den ogólny typ znakowy (tab. 3.2).
Tabela 3.2. Typy znakowe
Typ
B
Zbiór wartości
Liczba znaków
AnsiChar
P
1
znaki uporządkowane zgodnie z tabelą roz-
szerzonego zestawu znaków ASCII (0
÷
255)
256
WideChar
P
2
znaki uporządkowane zgodnie z tabelą zna-
ków standardu UNICODE (0
÷
65535)
65536
Char
O
1
znaki uporządkowane zgodnie z tabelą roz-
szerzonego zestawu znaków ASCII (0
÷
255)
256
P
– typ podstawowy,
O
– typ ogólny (zależny od rodzaju procesora i sys. oper.),
B – liczba bajtów pamięci potrzebna do przechowywania zmiennej tego typu
43
Przykłady deklaracji zmiennych typu znakowego:
var znak : Char;
litera : AnsiChar;
calka : WideChar;
Podstawowym
typem logicznym jest typ Boolean. Pozostałe typy logiczne,
wymienione w tabeli 3.3, stosuje się dla zapewnienia zgodności programów pas-
calowych z programami napisanymi w innych językach. Wartości typów logicz-
nych są oznaczane za pomocą dwóch predefiniowanych stałych
False i True.
Tabela 3.3. Typy logiczne
Typ
B
zbiór wartości (wartość porządkowa)
Boolean
P
1
False (0)
True (1)
ByteBool
O
1
False (0)
True (od 1 do 255)
WordBool
O
2
False (0)
True (od 1 do 65535)
LongBool
O
4
False (0)
True (od 1 do 4 294 967 295)
P
– typ podstawowy,
O
– typ ogólny (zależny od rodzaju procesora i sys. oper.),
B – liczba bajtów pamięci potrzebna do przechowywania zmiennej tego typu
Przykłady deklaracji zmiennych typu logicznego:
var decyzja : Boolean;
ok : Boolean;
czy_trojkat : Boolean;
Typy okrojone służą do ograniczenia zakresów wartości dowolnego z do-
tychczas opisanych typów porządkowych. Definicja typu okrojonego ma postać:
type identyfikator_typu = stala1 .. stala2;
Obie stałe muszą być tego samego typu porządkowego i stala1
≤
stala2.
Przykłady definicji typów okrojonych:
type litera = 'a' .. 'z';
type cyfra_dz = 0 .. 9;
type cyfra_zn = '0' .. '9';
type miesiac = 1 .. 12;
type klasy = (Ia, Ib, IIa, IIb, IIc, IId, IIIa, IIIb);
klasy_drugie = IIa .. IId;
44
Do typów prostych należą również standardowe
typy rzeczywiste (tab.
3.4), które nie są porządkowymi. Każdy z dostępnych typów rzeczywistych jest
dyskretnym i skończonym podzbiorem zbioru liczb rzeczywistych. Z tego po-
wodu nie wszystkie liczby rzeczywiste (z osi liczbowej) posiadają dokładną re-
prezentację w typach rzeczywistych. Maksymalna liczba cyfr znaczących man-
tysy wynosi 7, 15 albo 19 w sytuacji, gdy wyspecyfikowano kropkę dziesiętną.
Najmniejsza wartość liczb typu
Real, Single, Double, Extended jest, w przybli-
ż
eniu, równa największej wartości dodatniej pomnożonej przez (
−
1). Typ
Comp
stosuje się do zapamiętywania dużych liczb całkowitych. Kompilator traktuje
dane tego typu jako wartości rzeczywiste bez części wykładniczej. Typ
Curren-
cy służy do obliczeń pieniężnych, gdyż liczby tego typu mają postać dokładną.
Tabela 3.4. Typy rzeczywiste
Typ
B
Najmniejsza
wartość
dodatnia
Największa
wartość
dodatnia
Liczba
cyfr zna-
czących
mantysy
Real
O
4
albo
8
zależy
od
platformy
zależy
od
platformy
zależy
od
platformy
Single
P
4
2
-149
≈
1.40E
−
45
3.4E+38
7 albo 8
Double
P
8
2
-1074
≈
4.94E
−
324
1.7E+308
15 albo 16
Extended
P
10
2
-16445
≈
3.65E
−
4951
1.1E+4932
19 albo 20
wartość najmniejsza
wartość największa
Comp
P
8
−
2
63
=
−
9223372036854775808
2
63
−
1 =
9223372036854775807
19 albo 20
Currency
P
8
−
922337203685477.5808
922337203685477.5807
19
P
– typ podstawowy,
O
– typ ogólny (zależny od rodzaju procesora i sys. oper.),
B – liczba bajtów pamięci potrzebna do przechowywania zmiennej tego typu
Przykłady deklaracji zmiennych typu rzeczywistego:
var a, b, c : Single;
suma : Double;
moje_pi : Extended;
silnia : Comp;
kwota : Currency;
45
Zmienne
typu łańcuchowego służą do przechowywania ciągów znaków,
głównie typu
Char. Free Pascal pozwala definiować łańcuchy krótkie (takie jak
w Turbo Pascalu) oraz długie (tab. 3.5). Pamięć, dla łańcuchów krótkich, jest
przydzielana statycznie. Mogą one jednak zmieniać swoją długość (w trakcie
wykonywania programu) od 0 do maksymalnej długości N, podanej w definicji
typu krótkiego lub do wartości domyślnej równej 255.
Każdy znak łańcucha krótkiego zajmuje 1 bajt pamięci. Pierwszy znak
znajduje się pod indeksem 1, drugi – pod indeksem 2 itd. Po indeksem 0 jest
przechowywana informacja o aktualnej długości łańcucha.
Zmienna typu łańcucha długiego zajmuje w pamięci 4 bajty i jest wskaźni-
kiem do miejsca w pamięci, w którym znajduje się łańcuch. Długość łańcucha
nie jest bezpośrednio pamiętana, tzn. jest wyznaczana na żądanie użytkownika.
Tabela 3.5. Typy łańcuchowe
Typ
Długość
Uwagi
string[
N
]
2
≤
N
≤
255
Zawsze oznacza łańcuch krótki.
string
zależy od
{
$H}
Typ
string oznacza typ ShortString w stanie {$H
−
}.
Typ
string oznacza typ AnsiString w stanie {$H+}.
ShortString
255
Zawsze oznacza łańcuch krótki.
AnsiString
dowolna
Długość ograniczona tylko rozmiarem pamięci. Pamięć
przydzielana dynamicznie na stercie. Przechowuje znaki
typu
Char. Łańcuch ten jest zakończony znakiem pu-
stym (#0), którego jednak nie zalicza się do łańcucha.
WideString
dowolna
Ma te sami właściwości, co typ
AnsiString,
z tym, że przechowuje znaki typu
WideChar.
Standardowo dyrektywa {
$H} kompilatora jest włączona, tzn. ma stan {$H+}.
Elementami łańcucha są wartości typu
Char albo WideChar. Do poszcze-
gólnych elementów łańcucha można się odnosić tak, jak do elementów tablicy
−
poprzez indeks, np. s[1], s[2], s[10]. Typ łańcuchowy jest jedynym typem tabli-
cowym, na którym dopuszczalne są operacje agregujące (np. '+') oraz wartości
tego typu mogą być parametrami procedur
Write, Writeln, Read i Readln.
Przykłady deklaracji zmiennych typu łańcuchowego:
var imie : string[16];
s1, s2 : string;
linia : ShortString;
wydzial : AnsiString;
46
Typ tablicowy jest typem strukturalnym, czyli typem złożonym. Zmienna
typu tablicowego przechowuje elementy tego samego typu (prostego, łańcucho-
wego lub strukturalnego). Ogólna definicja typu tablicowego ma postać:
type identyfikator_typu = array [typy_indeksowe] of typ_skladowy;
Typy indeksowe są opisami typu porządkowego z wyjątkiem
Longint. Typ skła-
dowy oznacza dowolny typ. Przykłady definicji typów tablicowych:
{tablice 1-wymiarowe}
type IntList = array [1..100] of Integer;
type CharCode = array ['A'..'Z'] of Byte;
var A : IntList;
var C : CharCode;
…
A[10] := 14000; C['Z'] := 255; // przykłady u
ż
ycia
{tablice 2-wymiarowe}
type Matrix = array [0..10,
−
5..5] of Real;
Macierz = array [0..10] of array [
−
5..5] of Real;
{tablica 2-wymiarowa jako tablica tablic 1-wymiarowych}
type Tab = array [1..100] of Single;
Wynik = array [1.. 50] of Tab;
var M : Matrix;
var W : Wynik; {tablica 2-wymiarowa}
{przykłady odwoła
ń
do elementów tablicy 2-wymiarowej}
M[1,0] :=
−
1.043; // albo M[1][0] :=
−
1.043;
W[2,35] := 21.25; // albo W[2][35] := 21.25;
Odwoływanie się do poszczególnych elementów tablicy wymaga wprowa-
dzenia pojęcia selektora. W typie tablicowym, podobnie jak w łańcuchowym, se-
lektorem są nawiasy kwadratowe, w których podajemy indeks(y) elementu(ów),
do którego(ych) odwołujemy się.
W Lazarus FPC jest dopuszczalna
definicja zmiennych typu tablicowego
z wartościami początkowymi.
Przykłady takich definicji:
type Tabz = array [0..5] of Char;
var znaki : Tabz = ('a', 'z', 'A', 'X', '3', '#');
type Dane = array [1..4] of Smallint;
var liczby : Dane = (1, 2, 100, 5);
47
Dotychczas rozważane tablice były statyczne, tzn. ich rozmiar był znany
kompilatorowi przed kompilacją. Możliwe jest także definiowanie tablic o roz-
miarze określanym w trakcie wykonywania programu. Typ indeksowy takich ta-
blic jest domyślnie całkowity.
Tablice dynamiczne, domyślnie, indeksuje się
od 0. Tablice dynamiczne definiuje się następująco:
type identyfikator_typu = array of typ_skladowy;
Przykład definicji tablicy dynamicznej:
type tablica = array of Integer;
var A : tablica;
BEGIN
SetLength(A, 10); { High(A)
≡
9 }
A[0] := 15;
A[9] :=
−
24;
END.
Rekord jest złożoną strukturą danych, której składowe, zwane polami, mo-
gą być różnych typów. Pola rekordu również mogą być typu strukturalnego. Typ
rekordowy definiuje się następująco:
type identyfikator_typu = record
pole1 : typ1;
pole2 : typ2;
…
poleN : typN;
end;
Przykład definicji typu rekordowego:
type TOsoba = record
imie : string[20];
plec : Char;
wiek : Byte;
adres : array [1..10] of string;
end;
Cechy typu rekordowego:
•
rekordy mogą być zagnieżdżane, tzn. pole rekordu może być ponownie
typu rekordowego (struktura hierarchiczna),
•
pola tego samego typu mogą wystąpić jako oddzielone przecinkiem,
48
•
dostęp do poszczególnych pól zmiennej rekordowej odbywa się za po-
mocą kropki oraz nazwy pola (np. zespolona.re, zespolona.im),
•
podobnie jak w przypadku tablic, zmienna rekordowa, jako całość, nie
może być parametrem procedur:
Write, Writeln, Read, Readln.
•
zmienne rekordowe mogą wystąpić w instrukcji przypisania pod warun-
kiem, że są tego samego typu,
•
pole rekordu może występować we wszystkich operacjach, jakie są do-
puszczalne dla typu, jakiego jest to pole; w szczególności pole rekordu
może być parametrem procedury lub funkcji.
Typ zbiorowy (mnogościowy) jest zbiorem wszystkich podzbiorów danego
typu porządkowego. Typ zbiorowy definiuje się następująco:
type identyfikator_typu = set of typ_porzadkowy;
Liczba elementów typu porządkowego, zwanego też bazowym, występują-
cego w definicji typu zbiorowego, nie może przekraczać 256. Ponadto liczby po-
rządkowe tych elementów muszą należeć do przedziału [0, 255] (dlatego nie-
prawidłowa jest definicja:
set of [
−
5..5]). Wartości typu zbiorowego zapisuje się
przez podanie w nawiasach kwadratowych, w dowolnej kolejności, listy elemen-
tów danego zbioru. Zapis [ ] oznacza zbiór pusty.
Przykład 1. Definicja kilku typów zbiorowych.
type
CharSet = set of Char;
Digits = set of 0..9;
Day = (Sun, Mon, Tue, Wed, Thu, Fri, Sat);
Days = set of Day;
Przykład 2. Deklaracja i użycie zmiennych typu zbiorowego.
type znaki = set of Char;
var a, b, c : znaki;
z : Char;
BEGIN
a := ['0'..'9', 'a'..'z', 'A'..'Z'];
b := [ ]; // zbiór pusty
z := #27; // znak ASCII o kodzie równym 27
c := [z];
END.
49
Kompilator dopuszcza zmianę wartości wyrażenia jednego typu na wartość
innego typu. Operacja taka nazywa się
konwersją typu. Zmiany typu dokonuje
się za pomocą konstrukcji: identyfikator_typu (wyrażenie). Identyfikator typu
i wyrażenie muszą być równocześnie typu porządkowego albo wskaźnikowego.
W wyniku konwersji między dwoma niezgodnymi typami porządkowymi albo
dwoma niezgodnymi typami wskaźnikowymi może nastąpić obcięcie lub rozsze-
rzenie wartości przekształcanego wyrażenia.
Przykłady jawnych konwersji:
Integer('5') + Integer('6')
→
53 + 54 = 107
var b : Byte;
log : Boolean;
znak : Char;
c : Integer;
…
if not log and Boolean(b) then b := Byte('a');
znak := Char(65);
c := Byte('z') + 5;
Przykład niejawnej konwersji:
var c : Integer;
x, d : Double;
…
x := c + d;
W instrukcji
x := c + d;
zachodzi niejawna konwersja zmiennej
c
typu cał-
kowitego na zmienną typu rzeczywistego, ponieważ
d
jest typu rzeczywistego.
3.7.
Rodzaje operatorów
Aby zdefiniować w programie wyrażenie arytmetyczne, logiczne itp., obli-
czające pewną wartość, należy użyć sensownej kombinacji operandów i operato-
rów. Operandami (argumentami) są zmienne, stałe lub inne wyrażenia. Zbiór
operatorów jest natomiast ściśle określony. Operatory dzieli się m. in. na: aryt-
metyczne, logiczne, relacyjne, teoriomnogościowe i konkatenacji.
Operatory arytmetyczne służą do obliczania wartości wyrażeń liczbo-
wych. Typ zwracanego wyniku zależy od typu argumentów (tab. 3.6). Nie istnie-
ją operatory potęgowania ani pierwiastkowania. Operacje te można zrealizować
poprzez wielokrotne mnożenie albo za pomocą funkcji standardowych
Exp i Ln.
50
Tabela 3.6. Operatory arytmetyczne
Operator
Nazwa operacji
Typ argumentów
Typ wyniku
+
J
identyczność
całkowity
całkowity
rzeczywisty
Extended
−−−−
J
zmiana znaku
całkowity
całkowity
rzeczywisty
Extended
+
D
dodawanie
całkowity
całkowity
rzeczywisty
rzeczywisty
−
D
odejmowanie
całkowity
całkowity
rzeczywisty
rzeczywisty
*
D
mnożenie
całkowity
całkowity
rzeczywisty
rzeczywisty
/
D
dzielenie
całkowity
Extended
rzeczywisty
Extended
div
D
dzielenie całkowite
całkowity
całkowity
mod
D
reszta z dzielenia
całkowity
całkowity
J
– operator jednoargumentowy,
D
– operator dwuargumentowy
Dzielenie całkowite argumentu a przez argument b, czyli a
div b, zwraca:
•
największą liczbę całkowitą mniejszą lub równą ilorazowi a/b, gdy a/b
≥
0,
•
najmniejszą liczbę całkowitą większą lub równą ilorazowi a/b, gdy a/b < 0,
•
błąd, gdy b = 0.
15
div 6
→
2,
−
15
div 6
→
−
2, 15
div
−
6
→
−
2,
−
15
div
−
6
→
2.
Reszta z dzielenia argumentu a przez argument b, czyli a
mod b, jest okre-
ś
lona następująco: a
−
(a
div b)*b. Znak wyniku jest taki, jak znak argumentu a.
15
mod 6
→
3,
−
15
mod 6
→
−
3, 15
mod
−
6
→
3,
−
15
mod
−
6
→
−
3.
Jeżeli oba argumenty operacji +,
−
, *, /,
div, mod są różnych typów całko-
witych, to przed wykonaniem działania są one przekształcane to typu wspólne-
go, który jest standardowym typem całkowitym, mieszczącym w sobie wszyst-
kie możliwe wartości typów obu argumentów. Wynik też jest tego typu.
Operatory logiczne służą do wykonywania operacji logicznych na warto-
ś
ciach typów logicznych oraz na wartościach całkowitych (tab. 3.7). Jeżeli oby-
dwa argumenty operatorów
and, or lub xor są różnych typów całkowitych, to
typem wyniku jest typ wspólny obu argumentów.
51
Operacja a
shl b (a shr b) przesuwa bity liczby a o b bitów w lewo
(w prawo). Typ wyniku jest taki sam, jak typ operandu a.
Podczas przesuwania bitów w lewo, z prawej strony liczby pojawiają się
dodatkowe bity równe 0. Bity stojące na najbardziej znaczących pozycjach są
tracone. Podobnie jest w przypadku przesuwania bitów w prawo. Należy pamię-
tać, że podczas przesuwania bitów w prawo, bit znaku nie ulega powieleniu.
Operację
shl można stosować do szybkiego mnożenia liczby całkowitej
przez potęgę liczby 2, mianowicie: a
shl b
→
a
2
b
, np. 7
shl 5
→
7
2
5
= 224.
Operację
shr można stosować do szybkiego dzielenia całkowitego przez
potęgę liczby 2, mianowicie: a
shr b
→
a div 2
b
, np. 37
shr 3
→
37 div 2
3
= 4.
Tabela 3.7. Operatory logiczne
Operator
Nazwa operacji
Typ argumentów
Typ wyniku
not
J
negacja
całkowity
całkowity
logiczny
Boolean
and
D
koniunkcja
(logiczne i)
całkowity
całkowity
logiczny
Boolean
or
D
alternatywa
(logiczne lub)
całkowity
całkowity
logiczny
Boolean
xor
D
różnica symetryczna,
alternatywa wyklu-
czająca (e
xclusive
or), (logiczne albo)
całkowity
całkowity
logiczny
Boolean
shl
D
przesunięcie bitowe
w lewo (
shift left)
całkowity
całkowity
shr
D
przesunięcie bitowe
w prawo (
shift right)
całkowity
całkowity
J
– operator jednoargumentowy,
D
– operator dwuargumentowy
Jeżeli operandy operatora logicznego są typów logicznych, to wynik jest
określony zgodnie z tabelą 3.8.
Tabela 3.8. Wyniki operacji logicznych na argumentach typów logicznych
a
b
not a
not b
a
and b
a
or b
a
xor b
False
False
True
True
False
False
False
False
True
True
False
False
True
True
True
False
False
True
False
True
True
True
True
False
False
True
True
False
52
Jeżeli operandy operatora logicznego są typów całkowitych, to wynik jest
całkowity. Operacje logiczne wykonywane są na wszystkich parach bitów stoją-
cych na jednakowych pozycjach w obu operandach, zgodnie z tabelą 3.9.
Tabela 3.9. Wyniki operacji logicznych na parze bitów p i q liczb całkowitych
p
q
not p
not q
p
and q
p
or q
p
xor q
0
0
1
1
0
0
0
0
1
1
0
0
1
1
1
0
0
1
0
1
1
1
1
0
0
1
1
0
Przykłady użycia operatorów logicznych:
(a > 2) and (a < 9)
//
←
dobrze,
a > 2 and a < 9
//
←
ź
le
(rok mod 4 = 0) and (rok mod 100 <> 0)
or (rok mod 400 = 0)
not a in [1..5]
Operatory relacyjne służą do konstrukcji wyrażeń porównania. Są to na-
stępujące operatory dwuargumentowe:
= (równy), <> (różny, w matematyce
znany jako
≠
),
< (mniejszy), > (większy), <= (mniejszy lub równy, w matematy-
ce znany jako
≤
),
>= (większy lub równy, w matematyce znany jako
≥
),
in (jest
elementem zbioru, w matematyce znany jako
∈
).
Operatory <>, <=, >= należy pisać bez spacji między znakami!
Wynikiem wyrażenia porównania jest wartość
True, gdy relacja jest praw-
dziwa oraz wartość
False, gdy relacja jest fałszywa.
Za pomocą operatorów =,
<>, <, >, <=, >= można porównywać dwa argu-
menty o zgodnych typach prostych albo liczbę całkowitą z liczbą rzeczywistą
(np.: 5 = 5
→
True,
−
93.52 > 4
→
False, 0 >= 0.0
→
True, 7 <> 6.9
→
True).
Za pomocą operatorów =,
<>, <, >, <=, >= można porównywać dwa argu-
menty typu łańcuchowego (
ShortString, string) lub znakowego (Char), przy
czym jeden argument może być typu łańcuchowego, a drugi
−
znakowego. Znak
jest traktowany jak łańcuch o długości 1. Porównanie zachodzi pomiędzy zna-
kami znajdującymi się na tej samej pozycji w obu łańcuchach. Wartość znaku
jest równa jego wartości w rozszerzonej tabeli kodów ASCII.
Wynik relacji
porównania dwóch łańcuchów jest równy wynikowi porównania pierwszej
(licząc od początku łańcucha) pary różnych znaków, stojących na identycz-
nych pozycjach. Pusty znak jest mniejszy od każdego innego znaku.
53
Przykłady porównań łańcuchów:
'informatyk' < 'informatyka'
→
True
'400' = '400.0'
→
False
'b205' >= 'B206'
→
True
Na operandach a i b typu zbiorowego można wykonywać operacje:
=, <>,
<=, >=, in. Wyrażenie
a
= b ma wartość True, jeżeli oba zbiory zawierają te same elementy,
a
<> b ma wartość True, jeżeli zbiory różnią się co najmniej jednym elementem,
a
<= b ma wartość True, jeżeli zbiór a zawiera się w zbiorze b,
a
>= b ma wartość True, jeżeli zbiór b zawiera się w zbiorze a,
c
in a ma wartość True, jeżeli element c jest elementem zbioru a.
Operatory teoriomnogościowe +,
−
i * (tab. 3.10) służą do wykonywania
operacji na argumentach typu zbiorowego o zgodnych typach bazowych.
Tabela 3.10. Operatory teoriomnogościowe
Operator Nazwa operacji
Definicja operacji
+
suma zbiorów
(A
∪
B)
c jest elementem zbioru A + B,
jeżeli jest elementem zbioru A
lub jest elementem zbioru B
−
różnica zbiorów
( A \ B )
c jest elementem zbioru A
−
B,
jeżeli jest elementem zbioru A
i nie jest elementem zbioru B
*
iloczyn zbiorów
( A
∩
B )
c jest elementem zbioru A * B,
jeżeli jest elementem zbioru A
i jest elementem zbioru B
c
−
element typu porządkowego
Przykłady operacji na zbiorach:
[10, 12, 14, 8] + [9, 8, 10, 14]
→
[8, 9, 10, 12, 14]
[10, 12, 14, 8]
−
[9, 8, 10, 14]
→
[12]
[10, 12, 14, 8] * [9, 8, 10, 14]
→
[8, 10, 14]
[20..99]
−
[10..40]
→
[41..99]
['a', 'b'] + ['b', 'c', 'd']
→
['a', 'b', 'c', 'd']
Operator konkatenacji + użyty po każdym operandzie (z wyjątkiem ostat-
niego) służy do połączenia dwóch lub więcej łańcuchów w jeden dłuższy łań-
cuch. Argumenty muszą być typu łańcuchowego lub znakowego. Jeżeli wszyst-
54
kie operandy są łańcuchami krótkimi (
ShortString), wówczas łańcuch wyniko-
wy również jest łańcuchem krótkim. Jeżeli łańcuch powstały z połączenia łańcu-
chów krótkich zawiera więcej niż 255 znaków, to zostanie obcięty po 255 znaku.
Jeżeli co najmniej jeden łańcuch jest łańcuchem długim (
string), to wynik też
jest tego typu. Przykład: ’Lazarus’ + ’ ’ + ’FPC’
→
’Lazarus FPC’.
3.8.
Priorytet operatorów
Jeżeli w wyrażeniu występuje większa liczba operatorów i operandów,
to w celu określenia jednoznacznej kolejności wykonywania działań, twórcy ję-
zyka Pascal wprowadzili pewien
priorytet operatorów, czyli hierarchię ich
ważności (tab. 3.11). Aby wymusić inną kolejność operacji, niż wynika ona z
priorytetu operatorów, stosuje się nawiasy okrągłe, podobnie jak w matematyce.
Tabela 3.11. Priorytet operatorów (od najwyższego do najniższego)
Operatory
Kategoria operatorów
+,
−
, @,
not
jednoargumentowe
*, /,
div, mod, and, shl, shr, as
multiplikatywne
+,
−
,
or, xor
addytywne
=, <>, <, >, <=, >=, in, is
relacyjne
Obowiązują następujące zasady wiązania (przez kompilator) operandów do
operatorów:
•
argument występujący między dwoma operatorami o różnych prioryte-
tach jest wiązany z operatorem o wyższym priorytecie,
•
argument znajdujący się między dwoma operatorami o tym samym prio-
rytecie jest wiązany z operatorem stojącym z lewej strony.
Operacje z równymi priorytetami są wykonywane kolejno od lewej strony
do prawej, chociaż kompilator może czasami, bez ostrzeżenia, przestawić argu-
menty w celu wygenerowania optymalnego kodu wynikowego.
3.9.
Struktura programu
Program napisany w języku Free Pascal składa się z nagłówka, deklaracji
używanych w programie modułów, bloku i znaku kropki:
•
w nagłówku podaje się nazwę programu,
•
następnie deklaruje się moduły standardowe i użytkownika,
•
blok składa się z opisu danych i części wykonawczej,
•
kropka kończy tekst programu.
55
program nazwa_programu;
część deklaracyjna
część opisowa
begin
część wykonywalna
end.
Każda instrukcja programu jest zakończona średnikiem. Zazwyczaj układ
tekstu w linii czy też rozłożenie go na kilka linii nie ma znaczenia.
Część opisowa zawiera deklaracje typów, stałych, zmiennych, definicje
i deklaracje procedur i funkcji. Część wykonywalna – instrukcje do wykonania.
Przykład 1. Elementarny program wypisujący tekst na ekranie.
program wyklad02;
const max = 10;
var liczba : Byte;
BEGIN
writeln('Pocz
ą
tek');
liczba := 5 + max;
writeln('liczba =', liczba);
writeln('Koniec');
readln;
END.
Przykład 2. Program pokazuje użycie typów prostych.
program TypyProstePorz;
var
ok : Boolean;
x : 1..4;
dzien : (pon, wt, sr);
BEGIN
writeln(SizeOf(int64));
writeln(low(int64));
dzien := wt;
writeln(ord(dzien));
inc(dzien);
writeln(ord(dzien));
writeln(dzien=sr);
x := 4;
56
writeln(x);
dec(x);
writeln(x);
dec(x);
writeln(x);
writeln(high(x));
writeln(low(x));
readln;
END.
Przykład 3. Program ilustruje sposób użycia zmiennych typu zbiorowego.
program Typ_zbiorowy;
type zbior = set of Byte;
var x, z : zbior;
y : set of 0..9;
i : Integer;
BEGIN
x := [1,9,100,10]; {utworzenie zbioru}
i := 9;
y := [1,2,3,9,i+2]; {liczba 11 zostanie zignorowana}
z := [2,4,6,8];
writeln(i,' '); //
↓
u
ż
ycie typu zbiorowego
if not i in [1..5] then writeln ('tak')
else writeln ('nie');
i := i+1;
writeln(i); // i
→
10
if i in y then writeln ('tak') else writeln ('nie');
readln;
END.
57
4.
Instrukcje
Operacje wykonywane na danych są w programie opisywane za pomocą
instrukcji. Formalnie instrukcje dzieli się na proste i strukturalne.
Do instrukcji prostych zalicza się:
•
instrukcję przypisania,
•
instrukcję skoku,
•
instrukcję pustą,
•
instrukcję wywołania procedury.
Do instrukcji strukturalnych należą:
•
instrukcja złożona,
•
instrukcja warunkowa,
•
instrukcja iteracyjna,
•
instrukcja wyboru,
•
instrukcja wiążąca.
4.1.
Instrukcje proste
Do przypisania zmiennej wartości innej zmiennej lub wyrażenia służy
instrukcja przypisania, wykorzystująca symbol := (dwukropek i znak równo-
ś
ci). Wartość wyrażenia występującego po prawej stronie symbolu przypisania
musi być takiego samego typu, jak zmienna, do której następuje przypisanie.
Przykłady użycia instrukcji przypisania:
liczba := 10;
przyblizone_pi := 3.14;
s := 'Komentarz';
A[1,1] := 22;
liczba := liczba + 6; // liczba
→
16
znak := 'h';
wiersz[i] := 'H';
czy_koniec := True;
warunek := b*b-4*a*c > 0;
Wiek := 21;
P := 'Pierwsza';
s := ''; {ła
ń
cuch pusty, brak spacji mi
ę
dzy ''}
s := ' '; {jedna spacja mi
ę
dzy apostrofami}
s := 'Witaj'; {s[1]
→
'W', s[1] := 'w'; s
→
'witaj'}
58
Instrukcja skoku ma postać: goto etykieta; Zmienną etykieta należy zade-
klarować na początku programu jako:
label etykieta; a następnie można jej użyć
poprzedzając wybraną instrukcję nazwą etykiety i znakiem dwukropka, miano-
wicie: etykieta: instrukcja; Instrukcja
goto etykieta; spowoduje przekazanie ste-
rowania do przodu albo do tyłu w celu pominięcia lub powtórzenia określonego
zestawu instrukcji i kontynuowania wykonywania programu od miejsca ozna-
czonego napisem etykieta: instrukcja; Używanie instrukcji skoku nie jest zaleca-
ne, gdyż zmniejsza przejrzystość kodu źródłowego programu oraz utrudnia op-
tymalizację kodu przez kompilator. Instrukcja skoku może być łatwo zastąpiona
bardziej bezpieczną instrukcją iteracyjną.
Instrukcja pusta jest stosowana w sytuacji, gdy musimy użyć jakiejś in-
strukcji, ale w rzeczywistości nie chcemy nic obliczać. W Pascalu symbol śred-
nika oznacza czasem instrukcję pustą
−
instrukcja ta nie ma swojego symbolu.
Do wywołania procedury służy instrukcja:
nazwa_procedury; albo nazwa_procedury(lista_parametrów_aktualnych);
Parametry aktualne, czyli argumenty procedury, oddziela się przecinkami.
Przykłady wykorzystania popularnych procedur standardowych:
Writeln; // wypisuje pusty wiersz (pust
ą
lini
ę
)
Write; // wypisuje pusty znak
Write('a'); // wypisuje znak a
Write(a); // wypisuje warto
ść
zmiennej a
Writeln(x,2,y); { wypisuje warto
ść
zmiennej x,liczb
ę
2,
warto
ść
zmiennej y i przenosi kursor do nast
ę
pnej linii }
Read; // czyta pusty znak z klawiatury
Readln; // czyta „Enter” - przechodzi do nast
ę
pnej linii
Read(a); // czyta z klawiatury warto
ść
zmiennej a
Readln(a); { czyta z klawiatury warto
ść
zmiennej a i prze-
chodzi do nast
ę
pnej linii }
4.2.
Instrukcja złożona
Instrukcja złożona (blok instrukcji) jest ciągiem instrukcji poprzedzo-
nych słowem kluczowym
begin i zakończonych słowem kluczowym end:
begin
instrukcja_1;
instrukcja_2;
…
instrukcja_n
end
59
Przykład instrukcji złożonej:
begin
x := 3;
y := x+b;
z := x+y+z;
writeln(z);
end
4.3.
Instrukcja warunkowa
Instrukcja warunkowa uzależnia wykonanie pewnej instrukcji od spełnienia
podanego warunku. Występuje w dwóch wersjach: pełnej (tzw.
if
−−−−
then
−−−−
else)
oraz skróconej (tzw.
if
−−−−
then).
Wersja pełna instrukcji warunkowej ma postać:
if warunek then instrukcja_1 // tu nie daje się średnika
else instrukcja_2;
Oznacza to, że jeżeli spełniony jest warunek, wówczas zostanie wykonana
instrukcja_1, w przeciwnym wypadku zostanie wykonana instrukcja_2.
Wersja skrócona instrukcji warunkowej ma postać:
if warunek then instrukcja;
Oznacza to, że jeżeli spełniony jest warunek, wówczas zostanie wykonana
instrukcja, w przeciwnym wypadku instrukcja zostanie pominięta.
a)
b)
Rys. 4.1. Graficzna postać instrukcji warunkowej:
a) w wersji pełnej, b) w wersji skróconej
NIE
TAK
warunek
instrukcja
NIE
TAK
warunek
instrukcja_1
instrukcja_2
60
Wyrażenie warunek musi być typu logicznego (
Boolean), tzn. musi przyj-
mować wartość
True albo False.
Przykłady użycia instrukcji warunkowej:
if a > 5 then b := a-5;
if (a > 0) and (a <= 10) then x := 5 else x := a+5;
if (dzielnik <> 0) then wynik := dzielna/dzielnik
else writeln('Nie mo
ż
na dzieli
ć
przez zero!');
if (dzielnik <> 0) then
begin
wynik := dzielna/dzielnik;
writeln('dzielenie wykonano');
end
else writeln('Nie mo
ż
na dzieli
ć
przez zero!');
if zn in ['0'..'9'] then writeln('zn jest cyfr
ą
');
if not x in ['0'..'9'] then write('x nie jest cyfr
ą
');
if a = b then
begin
writeln('a i b s
ą
takie same');
obwod := 4*a;
end;
Przykład 1. Program sprawdza, czy liczba jest większa od 100.
program prog01;
var liczba : Integer;
BEGIN
write('Prosz
ę
poda
ć
liczb
ę
całkowit
ą
: ');
readln(liczba);
if liczba > 100 then
writeln('Liczba jest wi
ę
ksza od 100.')
else
writeln('Liczba jest mniejsza lub równa 100.');
readln;
END.
Przykład 2. Program szuka maksimum wśród 3 liczb rzeczywistych.
program instr_if_1;
61
var a, b, c, max : Single;
BEGIN
write('Podaj a '); readln(a);
write('Podaj b '); readln(b);
write('Podaj c '); readln(c);
if a >= b then if a >= c then max := a
else max := c
else if b >= c then max := b
else max := c;
write('max(a,b,c) = ', max:4:1);
readln;
END.
Przykład 3. Program zamienia ocenę numeryczną na ocenę tekstową.
Program Oceny;
var ocena : Byte;
BEGIN
write('Podaj ocen
ę
');
readln(ocena);
if ocena=6 then writeln('celuj
ą
cy')
else if ocena=5 then writeln('bardzo dobry')
else if ocena=4 then writeln('dobry')
else if ocena=3 then writeln('dostateczny')
else if ocena=2 then writeln('mierny')
else if ocena=1 then writeln('niedostateczny')
else writeln('Nieprawidłowa ocena!');
readln;
END.
4.4.
Instrukcja wyboru
W programowaniu często mamy do czynienia z sytuacją, gdy wykonanie
różnych operacji jest uzależnione od wartości pewnej zmiennej. Stosuje się wte-
dy instrukcję wyboru (tzw.
case – of). Składnia instrukcji wyboru ma postać:
case wyrażenie of
sekwencja_instrukcji_wyboru
end
albo
case wyrażenie of
sekwencja_instrukcji_wyboru
else instrukcja
end
62
Występujące w tej instrukcji wyrażenie nazywane jest selektorem. Jego
wartość musi być typu porządkowego. Sekwencja instrukcji wyboru zbudowana
jest z ciągu instrukcji. Każda z nich poprzedzona jest jedną lub kilkoma stałymi
wyboru, oddzielonymi od siebie przecinkiem
−
po nich występuje dwukropek.
Rys. 4.2. Graficzna postać instrukcji wyboru
Przykład 1. Program wczytuje znak i wypisuje jego rodzaj.
program instr_case1;
var znak : Char;
BEGIN
write('Prosz
ę
wpisa
ć
dowolny znak: ');
readln(znak);
case znak of
'0' .. '9' : writeln('Wpisano cyfr
ę
!');
'A' .. 'Z' : writeln('Wpisano wielk
ą
liter
ę
!');
'a' .. 'z' : writeln('Wpisano mał
ą
liter
ę
!');
'=' : writeln('Wpisano znak równo
ś
ci!');
'+','-' : writeln('Wpisano plus lub minus!');
else
writeln('Wpisano inny znak!')
end;
readln;
END.
Przykład 2. Program zamienia ocenę numeryczną na ocenę tekstową.
program instr_case2;
opcja_1 : ci
ą
g instrukcji_1
case
wyra
ż
enie
of
ci
ą
g instrukcji
end
else
opcja_2 : ci
ą
g instrukcji_2
opcja_N : ci
ą
g instrukcji_N
63
var ocena : Byte;
BEGIN
write('Podaj ocen
ę
');
readln(ocena);
case ocena of
6: writeln('celuj
ą
cy');
5: writeln('bardzo dobry');
4: writeln('dobry');
3: writeln('dostateczny');
2: writeln('mierny');
1: writeln('niedostateczny')
end;
readln;
END.
4.5.
Instrukcje iteracyjne
Instrukcja iteracyjna (pętla) stosowana jest w celu wielokrotnego wyko-
nania jednej lub więcej instrukcji. Są dostępne dwa rodzaje pętli:
•
pętle wykonujące się określoną liczbę razy,
•
pętle wykonujące się do momentu spełnienia pewnego warunku.
Pętla „
for” (instrukcja „dla”) wykonuje się określoną liczbę razy i występu-
je w dwóch wersjach:
for – to – do
(pętla licząca „w górę, aż do”) oraz
for – downto – do
(pętla licząca „w dół, aż do”).
Pełna składnia obu typów pętli „
for” ma postać:
for licznik_pętli := początek to koniec do zawartość_pętli;
for licznik_pętli := koniec downto początek do zawartość_pętli;
Najważniejszy cechy pętli „
for” to:
•
zmienna sterująca licznik_pętli musi być typu porządkowego,
•
zmienną licznik_pętli należy wcześniej zadeklarować,
•
wartości początek i koniec muszą być tego samego typu, co licznik_pętli,
•
literały początek i koniec mogą być stałymi lub wyrażeniami,
•
licznik_pętli zwiększa swą wartość o 1 po każdej iteracji pętli
to,
•
licznik_pętli zmniejsza swą wartość o 1 po każdej iteracji pętli
downto,
•
jeżeli koniec = początek, to zawartość_pętli wykona się tylko jeden raz,
64
•
jeżeli koniec > początek, to zawartość_pętli wykona się
koniec
−
początek + 1 razy,
•
jeżeli koniec < początek, to zawartość_pętli nie wykona się ani razu,
•
jeżeli zawartość_pętli zbudowana jest z kilku instrukcji, wtedy należy
zgrupować je za pomocą
begin ... end.
W Lazarus FPC, w instrukcji zawartość_pętli, nie może wystąpić instrukcja
przypisania do zmiennej licznik_pętli. Na przykład nieprawidłowa jest pętla:
for m := 1 to 10 do m := m+2;
Przykład 1. Program oblicza silnię.
program prog6;
var liczba : Integer;
silnia, i : Longint;
BEGIN
write('Podaj liczb
ę
naturaln
ą
:');
readln(liczba);
silnia := 1;
for i := 1 to liczba do silnia := silnia*i;
writeln('Warto
ść
silni dla ',liczba:2,' wynosi',silnia);
readln;
END.
Pętle, których działanie zależy od warunku to:
while
−
do („dopóki
−
wykonuj”) oraz
repeat
−
until („powtarzaj, aż do”).
Pętla
while
−
do służy do opisu iteracji ze sprawdzeniem warunku na po-
czątku i ma postać:
while warunek do instrukcja;
Cechy pętli
while
−
do:
•
warunek jest najczęściej wyrażeniem porównania,
•
warunek musi dawać wartość logiczną
True lub False,
•
jeżeli przed rozpoczęciem wykonywania pętli warunek ma wartość
Fal-
se, wtedy instrukcja nie wykona się ani razu,
•
instrukcja może być dowolną instrukcją prostą lub strukturalną,
•
jeżeli pętla ma wykonywać kilka instrukcji, to należy je zgrupować za
pomocą
begin … end,
•
instrukcja jest wykonywana tak długo, jak długo wartość warunku jest
równa
True,
65
•
przy korzystaniu z tej pętli należy zwracać uwagę na to, aby miała pra-
widłowo określony warunek, tzn. żeby prędzej czy później przyjął on
wartość
False, w przeciwnym razie pętla nigdy się nie przerwie (chyba,
ż
e zastosujemy procedurę
Break).
Rys. 4.3. Graficzna postać instrukcji iteracyjnej while
−
do
Przykład 2. Program wypisuje liczby: 0, 1, 2, …, 9.
program prog7;
var x : Integer;
BEGIN
x := 0;
while x < 10 do
begin
writeln(x);
x := x + 1;
end;
readln
END.
Przykłady 3. Program oblicza, ile co najmniej trzeba dodać kolejnych liczb na-
turalnych, aby ich suma była większa od podanej liczby.
program prog8;
var n, licznik, suma : Smallint;
BEGIN
write('Podaj liczb
ę
całkowit
ą
');
readln(n);
licznik := 0;
suma := 0;
while suma <= n do
begin
licznik := licznik + 1;
NIE
TAK
warunek
instrukcja
66
suma := suma + licznik;
end;
writeln('Minimalna ilo
ść
liczb to ', licznik);
readln;
END.
Przykład 4. Program oblicza NWD dwóch liczb metodą z odejmowaniem.
program prog1_NWD;
var n, m : Smallint;
BEGIN
write('Podaj liczb
ę
całkowit
ą
m: ');
readln(m);
write('Podaj liczb
ę
całkowit
ą
n: ');
readln(n);
while not (m = n) do
if m > n then m := m-n
else n := n-m;
writeln(' NWD wynosi: ', n);
readln;
END.
Rys. 4.4. Schemat blokowy algorytmu wyznaczania NWD metodą kolejnych dzieleń
NIE
TAK
m mod k
≠
0
lub
n mod k
≠
0
k
←
k
−
1
STOP
START
wczytaj m, n
k
←
m
wypisz k
67
Przykład 5. Program oblicza największy wspólny dzielnik według rys. 4.4.
program prog92_NWD;
var m, n, k : Integer;
BEGIN
writeln('Podaj m i n: ');
readln(m, n);
k := m;
while (n mod k <> 0) or (m mod k <> 0) do k := k-1;
writeln(k);
readln;
END.
Pętla
repeat
−
until służy do opisu iteracji ze sprawdzeniem warunku na
końcu i ma postać:
repeat
instrukcja_1;
instrukcja_2;
…
instrukcja_n
until warunek;
Cechy pętli
repeat
−
until:
•
instrukcje wewnątrz pętli są wykonywane co najmniej 1 raz,
•
po każdym wykonaniu instrukcji_n następuje sprawdzenie warunku,
•
instrukcje wewnątrz pętli wykonywane są tak długo, jak długo warunek
ma wartość
False,
•
zakończenie pętli następuje wtedy, gdy warunek ma wartość
True,
•
dla zapewnienia wyjścia z pętli trzeba zadbać o to, aby warunek chociaż
raz osiągnął wartość
True,
•
wcześniejsze zakończenie pętli jest możliwe przy użyciu
Break.
Rys. 4.5. Schemat blokowy instrukcji iteracyjnej repeat
−
until
TAK
NIE
warunek
instrukcja
68
Przykład 6. Program oblicza n
k
.
program prog10;
var
i, k : Integer;
n, x : Double;
BEGIN
write('Podaj podstaw
ę
n: ');
readln(n);
write('Podaj wykładnik k: ');
readln(k);
x := 1;
i := 1;
repeat
x := x*n;
i := i+1;
until i > k;
writeln('k-ta pot
ę
ga liczby n wynosi: ', x);
readln;
END.
Przykład 7. Program czyta znaki z klawiatury i podaje ich kod ASCII.
Program prog12a;
var zn : Char;
BEGIN
repeat
readln(zn);
writeln(zn, ' ', byte(zn));
until zn = #13;
writeln('Był Enter!');
readln;
END.
Przykład 8. Program czyta znaki z klawiatury i przerywa działanie, jeżeli wczy-
ta liczbę.
Program prog12b;
var zn : Char;
BEGIN
repeat
readln(zn);
writeln(zn, ' ', byte(zn));
until (zn >= '0') and (zn <= '9');
69
writeln('Była liczba!');
readln;
END.
Przykład 9. Ilustracja nieprawidłowego użycia pętli.
Program prog13;
var zn : Char;
k, x : Byte;
BEGIN
k := 5;
repeat
writeln(k);
k := k-1;
until k > 5; //warunek nigdy nie zostanie spełniony
k := 5;
x := 0;
repeat
writeln(k);
x := x+k;
until k < 0; //warunek nigdy nie zostanie spełniony
readln;
END.
Tabela 4.1. Zastosowanie trzech rodzajów pętli do tego samego zadania
Pętla
for
−−−−
to
−−−−
do
Pętla
repeat
−−−−
until
Pętla
while
−−−−
do
program p16;
const n=7;
var s,licznik:longint;
begin
s := 1;
for licznik:=2 to n do
s:=s*licznik;
write(' silnia= ',
s);
end.
program p17;
const n=7;
var s, licznik: Longint;
begin
s := 1;
licznik:=1;
repeat
s := s*licznik;
licznik
:= licznik+1;
until licznik>n;
write(' silnia= ',
s);
end.
program p18;
const n=7;
var s,licznik:longint;
begin
s:=1;
licznik:=2;
while licznik<=n do
begin
s := s*licznik;
licznik := licznik+1;
end;
write(' silnia= ',
s);
end.
70
Przy wyborze pętli warto kierować się poniższymi wskazówkami:
•
jeżeli instrukcja ma być wykonana określoną liczbę razy i zmienna ste-
rująca pętli ma się zwiększać, należy wybrać pętlę
for
−−−−
to
−−−−
do,
•
jeżeli instrukcja ma być wykonana określoną liczbę razy i zmienna ste-
rująca pętli ma się zmniejszać, należy wybrać pętlę
for
−−−−
downto
−−−−
do,
•
jeżeli warunkiem przerwania pętli jest wyrażenie logiczne i instrukcja
ma być wykonana przynajmniej raz, należy wybrać
repeat
−−−−
until.
•
jeżeli warunkiem przerwania pętli jest wyrażenie logiczne i nic nie wia-
domo o pierwszym wykonaniu instrukcji, należy wybrać
while
−−−−
do.
•
jeżeli nie wiadomo, którą pętlę wybrać, wtedy skuteczna będzie
while.
4.6.
Procedury Break i Continue
Czasem może zajść potrzeba natychmiastowego przerwania bieżącej iteracji
pętli bez wykonywania instrukcji, które pozostały do końca tej iteracji i zakoń-
czenia wykonywania pętli w ogóle. W inny przypadku może zajść potrzeba na-
tychmiastowego przerwania bieżącej iteracji i rozpoczęcia następnej. Do tych
obu celów służą
−
odpowiednio
−
procedury standardowe
Break i Continue.
Są one zdefiniowane w module
System.
Przykładowe użycie procedury
Continue:
while warunek_1 do
begin
Instrukcja_1;
if warunek_2 then Continue;
Instrukcja_2;
Instrukcja_3;
end;
Jeżeli warunek_2 będzie mieć wartość logiczną
True, wtedy wszystkie in-
strukcje w danej iteracji, występujące po wywołaniu procedury
Continue, zosta-
ną pominięte i rozpocznie się kolejna iteracja pętli.
Przykładowe użycie procedury
Break:
while warunek_1 do
begin
Instrukcja_1;
if warunek_2 then Break;
Instrukcja_2;
71
Instrukcja_3;
end;
Instrukcja_4;
Jeżeli warunek_2 będzie mieć wartość logiczną
True, wtedy nastąpi na-
tychmiastowe przerwanie procesu iteracji. Następną wykonaną będzie pierwsza
instrukcja poza pętlą, czyli Instrukcja_4.
Procedury
Break i Continue mogą być wywoływane tylko wewnątrz pętli.
Mogą być użyte wewnątrz każdej instrukcji złożonej występującej w pętlach
for,
repeat oraz while. Użycie tych procedur poza pętlą spowoduje błąd kompilacji.
Tabela 4.2. Przykłady użycia procedur Continue oraz Break
Użycie procedury
Continue
Użycie procedury
Break
program progr20;
const n = 20;
var licznik : Byte;
BEGIN
//s:=1;
licznik := 0;
while licznik < n do
begin
licznik := licznik+1;
if licznik mod 3 = 0
then continue;
writeln(licznik);
end;
readln;
END.
program progr21;
const n = 20;
var licznik : Byte;
BEGIN
//s:=1;
licznik := 1;
while licznik < n do
begin
licznik := licznik+1;
if licznik mod 5 = 0
then break;
writeln(licznik);
end;
readln;
END.
4.7.
Pętle i tablice
Pętle bardzo często stosuje się do danych typu tablicowego, dlatego poka-
ż
emy kilka przykładowych programów, w których są użyte pętle na tablicach.
Przykład 1. Program wpisuje do tablicy znaki o kodach ASCII.
Program tablice_znakow;
const max = 58; // 1-wymiarowa tablica znaków
type Tznak = array [1..max] of Char;
var znak : Tznak;
i : Byte;
72
BEGIN
for i:=1 to max do znak[i] := char(i+byte('A')-1);
for i:=1 to max do write(znak[i]);
readln;
END.
Przykład 2. Program, na różne sposoby, wpisuje dane liczbowe do tablic i wy-
pisuje dane z tablic na ekranie.
Program tablice_liczbowe;
const N = 5; {tablica 1-wymiarowa}
type TRtab = array [0..N] of Single;
TCtab = array [0..N] of Smallint; // zmie
ń
typ
var C : TCtab;
R : TRtab;
Cp : TCtab = (0,-1,4,20,-5,6);
i : Byte;
BEGIN
for i:=0 to N do C[i]:=i;//wypełnianie automatyczne
for i:=0 to N do
begin
write('R[',i,']=');
readln(R[i]); //czytanie z klawiatury
end;
for i:=0 to N do writeln(C[i]);//wypisanie w kolumnie
for i:=0 to N do write(Cp[i]:4);//wypisanie w wierszu
for i:=0 to N do writeln(R[i]:5:3);
readln;
//
↑
wypisanie+formatowanie
END.
Przykład 3. Program wpisuje dane liczbowe do tablicy dwuwymiarowej (macie-
rzy) i wypisuje dane z tablicy na ekranie.
Program macierze;
uses SysUtils;
const N = 4; {tablica 2-wymiarowa}
type TRMatrix = array [1..N, 1..N ] of Byte;
var M : TRMatrix;
i, j : Byte;
BEGIN
for i:=1 to N do
for j:=1 to N do M[i,j] := i+j-1;
for i:=1 to N do
73
begin
for j:=1 to N do write(M[i,j]:4);
writeln;
end;
readln;
END.
Do odwoływania się do poszczególnych pól rekordu lub pól i metod obiek-
tu służą desygnatory pól i metod, składające się z identyfikatora odpowiedniego
pola lub metody i nazwy zmiennej rekordowej lub obiektowej. Odwołanie takie
zwykle wydłuża tekst programu. Zastosowanie
instrukcji wiążącej pozwala na
wygodniejsze odwoływanie się do tych pól i poprawia przejrzystość programu.
Postać składni instrukcji wiążącej jest następująca:
with lista_zmiennych do instrukcja;
lista_zmiennych zawiera oddzielone przecinkami identyfikatory zmiennych re-
kordowych lub klasowych, a instrukcja może być dowolną instrukcją prostą lub
strukturalną. Jeżeli po słowie kluczowym
do występuje kilka instrukcji, to nale-
ż
y je zgrupować za pomocą
begin … end;
Instrukcja wiążąca wiąże pola typu rekordowego ze zmienną, do której na-
leżą. Instrukcja wiążąca może być zagnieżdżana.
Przykład 4. Użycie instrukcji wiążącej do zmiennych typu rekordowego.
program Dane_rekordy;
type Tdane = record
Imie : string[20];
Wiek : Byte;
Praca : array [1..10] of string;
end;
var prac1, prac2 : TDane;
BEGIN
prac1.imie := 'Leon';
prac1.wiek := 33;
prac1.praca[1] := 'Zaklad energetyczny';
prac1.praca[2] := 'Firma A';
prac2.imie := 'Daria';
prac2.wiek := 27;
prac2.praca[1] := 'S
ą
d rejonowy';
// wypisz dane o pracownikach
Write(prac1.imie, prac1.wiek);
74
writeln(prac1.praca[1], prac1.praca[2]);
// wypisz dane u
ż
ywaj
ą
c instrukcji with
with prac1, prac2 do
Writeln(imie, wiek, praca[1], praca[2]);
readln;
END.
4.8.
Zadania
Zad. 4.1. Napisać program porządkowania trzech liczb.
Zad. 4.2. Napisać program zamiany liczby dziesiętnej na szesnastkową.
Zad. 4.3. Napisać program zamiany liczby ósemkowej na dziesiętną.
Zad. 4.4. Napisać program wyznaczający najmniejszą wspólną wielokrotność
(NWW) dwóch liczb naturalnych, zgodnie ze wzorem (m
n)/NWD(m,n).
Zad. 4.5. Napisać program obliczania pierwiastka kwadratowego z liczby nieu-
jemnej a (
a
x
=
), z zadaną dokładnością eps, iteracyjną metodą Newtona
−
=
+
i
i
i
x
x
a
x
2
1
1
. Wartość
a , czyli
1
+
i
x
, ma dokładność eps, jeżeli
eps
x
a
i
<
−
2
.
i
x oznacza kolejne przybliżenie a .
Zad. 4.6. Napisać program obliczający sumę szeregu
∑
=
+
+
=
100
1
2
1
i
i
x
i
s
(x dowolne).
Zad. 4.7. Napisać program obliczania wyrażenia danego wzorem
L
L
7
5
5
3
3
1
6
6
4
4
2
2
2
π
⋅
⋅
⋅
⋅
⋅
⋅
⋅
⋅
⋅
⋅
=
Zad. 4.8. Napisać program, który wyznaczy wartość maksymalną i wartość mak-
symalną modułu elementów tablicy
A. Tablica ma N elementów typu rze-
czywistego tj.
A[i]
∈
R dla i=1, 2, …, N.
Zad. 4.9. Dla danej tablicy dwuwymiarowej
A[i, j], i, j = 1..N wyznaczyć tabli-
cę transponowaną (kolumny zamienione na wiersze). Rozważyć dwa przy-
padki: tablica inicjalizowana jest podczas deklaracji w kodzie źródłowym
albo tablica jest wczytywana z klawiatury.
75
5.
Procedury i funkcje
Podprogram to wyodrębniona część programu, stanowiąca całość, posia-
dająca nazwę i ustalony sposób wymiany informacji z pozostałymi częściami
programu. Ze względu na sposób wymiany informacji (lub jego brak) z pozosta-
łą częścią programu, podprogramy dzielimy na procedury i funkcje.
Funkcje i procedury:
•
umieszcza się w części deklaracyjnej programu lub w odrębnej jednost-
ce kompilacyjnej,
•
funkcja zazwyczaj operuje na pewnych argumentach (ale nie musi)
i zwraca pewną, obliczoną wartość (lub zestaw wartości),
•
funkcja może modyfikować argumenty, chociaż zazwyczaj tylko wyko-
rzystuje do obliczeń przekazane jej wartości,
•
procedura jest podobna do funkcji, z tym, że nie może zwracać wartości
w sposób zarezerwowany dla funkcji,
•
procedura może posiadać argumenty lub nie, może zwracać wartości
do „świata zewnętrznego”, ale tylko w określony sposób.
5.1.
Składnia procedury
Procedura to podprogram wykonujący jedną lub więcej czynności i zwra-
cający od jednego do kilku wyników lub niezwracający żadnego wyniku. Nazwa
procedury jest używana w charakterze instrukcji w programie.
Składnia definicji procedury jest następująca:
procedure nazwa_procedury (lista_parametrów_formalnych);
część_deklaracyjna
begin
ciąg_instrukcji
end;
Część deklaracyjna oraz lista parametrów formalnych są opcjonalne, tzn.
można ich nie definiować, jeżeli nie są potrzebne. Część deklaracyjna procedury
może zawierać te same elementy, co część opisowa programu tzn.: definicje ty-
pów, stałych, zmiennych, ale nie klas. Parametry formalne oddziela się średni-
kami. Lista parametrów formalnych określa również, w jaki sposób parametry
formalne zostaną zastąpione parametrami aktualnymi w trakcie wywołania pro-
cedury. Parametry aktualne w wywołaniu procedury oddziela się przecinkami.
76
Parametry formalne pojawiają się w nagłówku deklaracji funkcji bądź
procedury i są używane w treści funkcji lub procedury jak zwykłe zmienne.
Liczba parametrów formalnych nie jest ograniczona i ich kolejność występowa-
nia jest dowolna. Parametr formalny jest obowiązkowo zmienną.
Parametry aktualne są parametrami występującymi w bloku, z którego
wywoływana jest funkcja lub procedura i używane są podczas wywołania pro-
cedury lub funkcji. Liczba parametrów aktualnych musi być taka sama, jak licz-
ba parametrów formalnych w nagłówku i musi zachodzić zgodność typów.
Parametrem aktualnym może być wyrażenie, zmienna albo stała.
Rys. 5.1. Schemat blokowy składni procedury
Ważne uwagi dotyczące wywoływania (używania) procedur w programie:
•
wywołanie procedury polega na użyciu w programie jej nazwy z listą
parametrów aktualnych (jeżeli istnieje) w nawiasach okrągłych:
nazwa_procedury (lista_parametrów_aktualnych);
przykład:
wypisz (a, b, c);
•
parametry aktualne oddziela się przecinkami,
•
typy parametrów aktualnych muszą być zgodne z typami odpowiadają-
cych im parametrów formalnych,
•
lista parametrów formalnych pozwala w sposób jawny przekazywać da-
ne do procedury lub funkcji.
nagłówek
procedury
begin
ci
ą
g instrukcji
end
cz
ęść
deklaracyjna
blok
procedury
77
5.2.
Przekazywanie parametrów do procedur i funkcji
Zastępowanie parametrów formalnych aktualnymi jest nazywane przekazy-
waniem parametrów do procedury lub funkcji
. Wyróżnia się trzy główne sposo-
by przekazywania parametrów: przez wartości, przez zmienne i przez stałe.
Parametry przekazywane przez wartości deklaruje się w nagłówku pro-
cedury (lub funkcji) następująco:
procedure nazwa_procedury
(Identyfikator_1 : typ_1; Identyfikator_2 : typ_2; ...; Identyfikator_n : typ_n);
Przekazywanie
przez wartość to przekazywanie w jedną stronę, tzn. praca
na kopii parametru. Parametry formalne przekazywane przez wartości (Identyfi-
kator
_1, Identyfikator_2 itd.) są, w obrębie procedury (lub funkcji), traktowane
jak zmienne lokalne, którym nadaje się wartości początkowe równe wyliczo-
nym, w chwili wywołania, wartościom wyrażeń stanowiących parametry aktual-
ne.
Operacje wykonywane w treści funkcji lub procedury są wykonywane
zatem na kopii umieszczonej na stosie i w związku z tym nie powodują
zmian wartości odpowiadających im parametrów aktualnych.
Przykłady deklaracji procedur z parametrami przekazywanymi przez wartości:
procedure dodaj (a, b : Integer; c : Single);
procedure generator (x : Real);
procedure Drukuj(linia1, linia2 : string); // ShortString jest niedozwolony
procedure WypiszDate; // procedura bez parametrów
type Matrix = array [1..5, 1..4] of Double;
procedure Zapisz(M : Matrix);
Parametry przekazywane przez zmienne deklaruje się w nagłówku
procedury (lub funkcji) następująco:
procedure nazwa_procedury (var Identy-
fikator
_1 : typ_1;
var Identyfikator_2 : typ_2; ...; var Identyfikator_n : typ_n);
Oprócz standardowych typów można użyć także słów kluczowych
string
i
file.
Argumenty aktualne, odpowiadające parametrom formalnym przeka-
zywanym przez zmienne, muszą być zmiennymi.
Użycie parametru przekazywanego przez zmienną oznacza pracę na
oryginale parametru aktualnego. Operacje wykonywane w treści funkcji lub
procedury na parametrach przekazywanych przez zmienne są wykonywane
(w momencie wywołania procedury lub funkcji) bezpośrednio na przekazywa-
nych do procedury lub funkcji parametrach aktualnych.
Jeżeli w treści proce-
dury lub funkcji zostaną użyte instrukcje zmieniające wartość parametru
78
formalnego, to identyczne zmiany dokonają się również w parametrze ak-
tualnym po wywołaniu procedury lub funkcji.
Przykłady deklaracji procedur z parametrami przekazywanymi przez zmienną:
procedure zapisz(var x : Byte);
procedure dodaj(var a : Integer; var b : Single);
procedure dodaj(a, b : Integer; var c : Single); // tylko c przez zmienną
type Matrix = array [1..5, 1..5] of Byte;
procedure dodajM(A, B : Matrix; var M : Matrix); // tylko M
−
Dzięki parametrom przekazywanym przez zmienne procedury mogą,
w pewnym sensie, zwracać wartości.
Parametry typu plikowego muszą być przekazywane przez zmienną.
Dopuszczalne jest, aby część parametrów była przekazywana przez war-
tość, a reszta przez zmienne.
Parametry przekazywane przez stałe deklaruje się w nagłówku procedu-
ry (lub funkcji) następująco:
procedure nazwa_procedury (const Identyfika-
tor
_1 : typ_1;
const Identyfikator_2 : typ_2; ...; const Identyfikator_n : typ_n);
Parametry przekazywane przez stałe są w treści procedury lub funkcji
traktowane jako zmienne lokalne przeznaczone tylko do odczytu. W treści
procedury lub funkcji nie jest dozwolone przypisywanie im jakichkolwiek war-
tości. Zastosowanie instrukcji przypisania do parametru przekazywanego przez
stałą wygeneruje błąd na etapie kompilacji.
Dla parametrów typu łańcuchowego i strukturalnego zaleca się stosować
przekazywanie przez stałe, gdyż kompilator generuje dla nich bardziej efektyw-
ny kod.
Parametry otwarte umożliwiają przekazywanie do tej samej procedury lub
funkcji tablic o różnych rozmiarach. Deklaracja tablicowego parametru otwarte-
go ma postać:
var nazwa_zmiennej : array of nazwa_typu lub
const nazwa_zmiennej : array of nazwa_typu lub
nazwa_zmiennej
:
array of nazwa_typu
w zależności od tego, czy deklarowany parametr jest przekazywany przez
zmienną, stałą czy wartość.
79
W treści procedury lub funkcji tablicowy parametr otwarty jest traktowany
jak
array [0..n-1] of nazwa_typu, gdzie n oznacza liczbę elementów argumentu.
W treści procedury lub funkcji, do określenia liczby elementów tablicy
otwartej, stosuje się funkcję standardową
High. Do tej tablicy można też stoso-
wać funkcje
SizeOf i Low.
Tabela 5.1. Program z definicjami procedur, demonstrujący ich działanie
program Proc1;
procedure www(k, m : Integer);
begin // przekazywanie przez wartości
writeln('k=', k);
k := k+m;
writeln('k=', k);
end;
procedure zzz(var k, m : Integer);
begin // przekazywanie przez zmienne
writeln('k=', k);
k := k+m;
writeln('k=', k);
end;
var // c.d.
a, b : Integer;
BEGIN
a := 1;
b := a+1;
www(a, b+1);
writeln('a=', a);
www(a, b+1);
writeln('a=', a);
zzz(a, b);
writeln('a=', a);
zzz(a, b);
writeln('a=', a);
readln;
END.
Przykład 1. Generowanie i wypisywanie zawartości macierzy.
program Proc3;
uses SysUtils;
const RM = 5;
type matrix = array [1..RM, 1..RM] of Byte;
procedure gener(var M : matrix);
var i, k : Byte; //zmienne lokalne
begin
Randomize;
for i:=1 to RM do
for k:=1 to RM do
M[i,k] := random(10);
end;
procedure wypisz(M : matrix);
var i, k : Byte;
80
begin
for i:=1 to RM do
begin
for k:=1 to RM do write(M[i,k]:4);
writeln;
end;
writeln;
end;
var A, M : Matrix;
BEGIN
gener(M);
wypisz(M);
gener(A);
wypisz(A);
readln;
END.
W Lazarus FPC istnieje możliwość podania
domyślnej wartości parame-
tru wywołania procedury lub funkcji przez użycie słowa kluczowego const
w nagłówku przed nazwą parametru oraz podanie jego wartości po znaku =.
Przykład 3. Procedura z domyślną wartością parametru wywołania.
program Proc2;
Procedure demo(const napis : string = 'Domyslny');
begin
writeln(napis);
end;
var s : string;
BEGIN
s := 'procedura demo';
demo(s);
demo;
demo('dowolny napis');
readln;
END.
5.3.
Procedury i dyrektywa zapowiadająca Forward
Dyrektywy zapowiadającej
Forward używa się w celu zadeklarowania
podprogramu, który zostanie zdefiniowany później po to, aby w podprogramach
zdefiniowanych wcześniej można było użyć wywołania tego podprogramu.
81
Przykład użycia dyrektywy zapowiadającej Forward:
Program proc5;
var a : Byte;
procedure druga (var y : Byte); forward;
function pierwsza (var x : Byte) : Byte;
begin
inc(x);
if x < 10 then druga(x); // Result := x;
end;
procedure druga (var y : Byte); // forward;
begin
pierwsza(y);
end;
BEGIN
a := 4; // warto
ść
pocz
ą
tkowa
pierwsza(a);
writeln(a); // warto
ść
ko
ń
cowa
readln;
END.
5.4.
Składnia funkcji
Funkcja to podprogram wykonujący jedną lub więcej czynności i zwraca-
jący jeden typ wartości, ale niekoniecznie tylko jedną wartość. Może to być np.
tablica wartości. Nazwa funkcji używana jest w charakterze wyrażenia lub in-
strukcji.
82
Rys. 5.2. Schemat blokowy składni funkcji
Podstawowa składnia definicji funkcji jest następująca:
function nazwa_funkcji (lista_parametrów_formalnych) : typ_wyniku;
// deklaracja lokalnych typów (
type), stałych (const) i zmiennych (var)
Begin
Ciąg_instrukcji;
Result := wyrażenie; // albo nazwa_funkcji := wyrażenie;
End;
Lista parametrów formalnych może być pusta. Wówczas nawiasy okrągłe
można opuścić albo pozostawić.
Ponieważ
funkcja zwraca pewną wartość, to w treści funkcji musi wystą-
pić co najmniej jedna instrukcja przypisania postaci:
nazwa_funkcji
:= wyrażenie; albo
Result := wyrażenie;
która powoduje przypisanie wartości wyrażenie nazwie funkcji. Identyfikator
Result jest domyślną zmienną lokalną w funkcji i nie ma potrzeby jej deklaro-
wać. Typ zmiennej
Result jest taki sam, jak typ_wyniku!
Identyfikator
Result może wystąpić wiele razy w treści funkcji, ale jako
wartość zwracana przez funkcję będzie ta, która jako ostatnia została przypisana
do zmiennej
Result. Należy tak używać instrukcji warunkowych i pętli w treści
funkcji, aby w każdej sytuacji, tzn. bez względu na wartości logiczne warunków
występujących w treści funkcji, zawsze była przypisana pewna wartość do
zmiennej
Result. Tak jak każda zmienna lokalna, również zmienna Result po-
nagłówek
funkcji
begin
ci
ą
g instrukcji
end
cz
ęść
deklaracyjna
blok
funkcji
Result
←
wyra
ż
enie
83
winna być zainicjalizowana przed pierwszym użyciem, w przeciwnym razie
wartość tej zmiennej jest nieokreślona (losowa).
Wykonanie instrukcji
Result := wyrażenie; nie kończy działanie funkcji,
lecz tylko ustala wartość zwracaną przez funkcję. Wszystkie instrukcje, które
znajdują się dalej w treści funkcji są wykonywane, aż do słowa kluczowego
end; Wystąpienie instrukcji Result := wyrażenie_2; powoduje zmianę wartości
zwracanej przez funkcję.
Wywołanie funkcji polega na wpisaniu w kodzie źródłowym programu na-
zwy funkcji z odpowiednią listą parametrów aktualnych:
nazwa_funkcji (lista_parametrów_aktualnych);
Parametry aktualne oddziela się przecinkami.
Jeżeli typ_wyniku nie zgadza się z typem wartości podstawianej pod zmien-
ną
Result, wówczas wartość podstawiana jest odpowiednio konwertowana, jeśli
jest to możliwe.
Uwagi odnośnie parametrów formalnych i aktualnych oraz na temat sposo-
bów przekazywania parametrów do funkcji są takie same jak dla procedur.
Przykład funkcji z parametrami a i b, przekazywanymi przez wartość:
function dodaj (a : Byte; b : Single) : Single;
begin
Result := a+b; // albo dodaj := a+b;
end;
Przykład wywołania funkcji
dodaj:
var a, x : Byte;
b, y, wynik :
Single;
...
wynik :=
dodaj(x, y);
wynik :=
dodaj(x, dodaj(x, y));
Przykład ilustrujący przekazywanie tablic do funkcji:
type Wektor = array [1..10] of Smallint;
function Iloczyn_skalarny (A, B : Wektor) : Smallint;
84
Przykład wywołania funkcji Iloczyn_skalarny:
var A, B : Tablica;
wynik :
Smallint;
...
wynik :=
Iloczyn_skalarny(A, B); writeln(’wynik=’, wynik);
// albo
writeln(’wynik=’,
Iloczyn_skalarny(A, B));
Przykład 1. Wywołanie funkcji z parametrami przekazywanymi przez wartość.
program Podpr2;
function suma2(k, m : Integer) : Integer;
begin
Result := k+m;
Result := 2*Result;
//suma2 := 2*(k+m); // niepolecane
end;
var a, b, x : Integer;
BEGIN
x := suma2(1,4); //wywołanie funkcji suma2
writeln('suma=', x);
a := 2; b := 3;
writeln(suma2(2,3)); //wywołanie funkcji suma2
writeln(suma2(a,b)); //wywołanie funkcji suma2
readln;
END.
Przykład 2. Funkcja
wypelniacz
wypełnia tablicę
A
podaną liczbą
x
.
program Podpr5;
type TArr = array [1..10] of Integer;
function wypelniacz(x : Integer) : TArr;
var i : Integer;
begin
for i := 1 to 10 do Result[i] := x;
end;
var A : TArr;
i : Byte;
BEGIN
A := wypelniacz(2);
for i := 1 to 10 do write(A[i], ' ');
readln;
END.
85
5.5.
Przeciążanie funkcji
Przeciążanie (przeładowywanie) funkcji polega na definiowaniu kilku
funkcji lub procedur o tych samych nazwach różniących się zestawem parame-
trów formalnych wywołania. W takim przypadku trzeba użyć dyrektywy
over-
load na końcu nagłówka każdej przeciążonej funkcji lub procedury.
O tym, którą przeciążoną funkcję wywołać, decyduje kompilator, ba-
dając typy parametrów aktualnych użytych w konkretnym wywołaniu.
Funkcji i procedur o tych samych nazwach i o jednakowych listach parame-
trów formalnych, ale różnych typach wartości zwracanej nie wolno przeciążać.
Przykład 1. Przeciążanie funkcji
dodaj
.
program Podpr4;
function dodaj(a,b : Byte) : Byte; Overload;
begin
Result := a+b;
end;
function dodaj(a,b : string) : string; Overload;
begin
Result := a+b;
end;
BEGIN
writeln('dodawanie liczb =', dodaj( 1, 2 ));
writeln('dodawanie łancuchow =', dodaj('1', '2'));
readln;
END.
Przykład 2. Funkcja zamiany liczby naturalnej na kod o podstawie 2
÷
10.
Program fun_4;
const MaxPodst = 10;
type TPodst = 2..MaxPodst;
function Card2Str(x : Cardinal; p : TPodst) : string;
type TCyfra = 0..MaxPodst-1;
var cyfra : TCyfra;
begin
Result := '';
repeat
cyfra := x mod p;
x := x div p;
Result := char(cyfra + byte('0')) + Result;
until x = 0;
end;
86
var a : Cardinal;
b : TPodst;
BEGIN
write('a=');
readln(a);
write('podstawa systemu liczenia:');
readln(b);
writeln(Card2Str(a, b));
readln;
END.
5.6.
Wywołanie funkcji przez inną funkcję
W kodzie źródłowym funkcji może wystąpić wywołanie innej funkcji. Wy-
konywanie bieżącej funkcji jest wówczas chwilowo przerywane, a rozpoczyna
się wykonywanie drugiej. Po zakończeniu wykonania funkcji drugiej sterowanie
wraca do pierwszej funkcji, do miejsca gdzie została przerwana. Tak więc funk-
cje i procedury można zagnieżdżać.
W treści funkcji lub procedury można umieścić wywołanie innej funkcji lub
procedury, która została zdefiniowana wcześniej.
W części deklaracyjnej funkcji (procedury) tzw. zewnętrznej można zdefi-
niować inną funkcję (procedurę) tzw. wewnętrzną, która będzie lokalna, tzn. wi-
doczna tylko w treści procedury zewnętrznej.
Wywołanie funkcji A
w treści funkcji B.
Definicja funkcji B w czę-
ś
ci deklaracyjnej funkcji A
i wywołanie funkcji B w
treści funkcji A.
Wywołanie procedury A
w procedurze B.
function A:pewien_typ;
begin
...
end;
function B;
var x : pewien_typ;
begin
...
x :=
A;
...
end;
function A;
function B:pewien_typ;
begin
...
end;
var x : pewien_typ;
begin
...
x :=
B;
...
end;
procedure A;
begin
...
end;
procedure B;
begin
...
A;
end;
87
5.7.
Zmienne globalne i lokalne
Zmienne globalne to zmienne zdefiniowane na zewnątrz procedur, funkcji
oraz metod. Zmiennych globalnych można używać we wszystkich funkcjach
i procedurach pliku źródłowego, w którym zostały zadeklarowane. Wszystkie
zmienne globalne są umieszczane w tzw. segmencie danych, dla którego pamięć
jest przydzielana tylko raz, w chwili rozpoczęcia wykonywania programu.
Pamięć ta jest zerowana na starcie programu, tzn.
zmiennym globalnym są
przypisywane wartości zerowe odpowiedniego typu. Tak więc, jeżeli progra-
mista zapomni zainicjalizować zmienną globalną, to istnieje duża szansa, że
program będzie działał prawidłowo, gdyż bardzo często pierwszą przypisywaną
(celowo) wartością do zmiennej globalnej jest właśnie wartość zerowa.
Powinno się unikać używania zmiennych globalnych w treści procedur
i funkcji, gdyż utrudnia to śledzenie poszczególnych fragmentów kodu i popra-
wianie błędów (debugowanie). Zmienną globalną powinno przekazywać się do
procedur lub funkcji:
•
przez wartość, jeżeli chcemy działać na kopii zmiennej globalnej,
•
przez zmienną, jeżeli chcemy zmienić wartość tej zmiennej globalnej,
•
przez stałą, jeżeli chcemy tylko odczytać wartość zmiennej globalnej.
Z
mienne lokalne to wszystkie pozostałe zmienne w programie, tzn. te,
które nie są globalne. Właściwości zmiennych lokalnych są następujące:
•
zmienne lokalne są umieszczane w tzw. segmencie stosowym, czyli w
obszarze pamięci podręcznej (w innym obszarze niż zmienne globalne),
•
przy każdorazowym wywołaniu funkcji zmiennym lokalnym przydzie-
lana jest na stosie pamięć, a po zakończeniu wykonywania funkcji pa-
mięć ta jest zwalniana,
•
wewnątrz funkcji można definiować tylko zmienne lokalne,
•
zmienna lokalna danej funkcji jest widoczna tylko w tej funkcji, w któ-
rej została zadeklarowana,
•
nie można w jednej funkcji odwołać się do zmiennej lokalnej zadekla-
rowanej w innej funkcji,
•
wiele funkcji może mieć zmienną lokalną o takiej samej nazwie,
•
nazwy zmiennych lokalnych i globalnych mogą być jednakowe (wtedy
jednak zmienne lokalne przysłaniają zmienne globalne),
•
zmienne lokalne nie są zerowane przy „powoływaniu do życia”, tzn.
mają wartość losową, która przy każdym kolejnym uruchomieniu pro-
gramu może być inna niż poprzednio,
88
•
po zakończeniu wykonywania bloku instrukcji, dla którego została po-
wołana do życia zmienna lokalna, przestaje ona istnieć (jest usuwana ze
stosu),
•
przy ponownym wejściu do danego bloku (np. przy ponownym wywo-
łaniu tej samej funkcji) zmienna lokalna jest powoływana do życia po
raz kolejny,
•
przerwanie wykonywania jednej funkcji przez wywołanie innej, a nawet
tej samej, nie powoduje zlikwidowania zmiennych lokalnych pierwszej
funkcji znajdujących się już na stosie. Oznacza to, że mogą istnieć na
stosie dwie zmienne lokalne o jednakowej nazwie należące do tej samej
funkcji, ale posiadające różne wartości. Zadaniem kompilatora jest pra-
widłowe rozróżnienie tych zmiennych.
•
zmienne lokalne nie mogą mieć nazw takich, jak nazwy parametrów
formalnych funkcji.
Takie same uwagi odnoszą się do zmiennych lokalnych w procedurach.
Przykład 1. Program z jedną zmienną lokalną i jedną zmienną globalną.
program proc4;
var zm_globalna : Integer;
Procedure proc_glob;
var zm_lok : Integer;
begin
zm_lok := 1;
zm_globalna := zm_globalna+1;
writeln('zm_lok=', zm_lok);
writeln('zm_glob=', zm_globalna);
end;
BEGIN
zm_globalna := 5;
writeln( 'zm_glob=', zm_globalna );
proc_glob;
writeln( 'zm_g=', zm_globalna ); // 6
zm_globalna := 9;
writeln( zm_globalna ); // 9
proc_glob; // 10
writeln( zm_globalna ); // 10
readln;
END;
89
Przykład 2. Przysłanianie zamiennych globalnych przez zmienne lokalne.
program fun_zmienne;
var x, y : Byte; // zmienne globalne
Function zewn(a : Byte) : Byte;
var x : Byte; // zmienna lokalna
function wewn(a : Byte) : Byte;
var x : Byte; // zmienna lokalna
begin
x := 1;
x := x+1;
Result := x;
end;
Begin // funkcja zewn
x := 10;
x := x + wewn(a);
Result := x;
End;
BEGIN
x := 100;
y := x + zewn(x);
{Jaka b
ę
dzie warto
ść
y? Za pomoc
ą
klawisza F7 i okna
Czujki nale
ż
y prze
ś
ledzi
ć
warto
ś
ci zmiennych y i x.}
writeln('Jaki wynik? ..', y);
readln;
END.
Przykład 3. Przekazywanie parametrów przez zmienną.
program z1fun;
var x, y : Integer;
Function zad1(a : Integer; var b : Integer) : Integer;
function wew1(c, d : Integer) : Integer;
begin
d := c+d;
wew1 := d+x; // albo Result := d+x;
end;
Begin
a := a+b;
b := a+b;
Result := wew1(a,b); // albo zad1 := wew1(a,b);
End;
90
BEGIN
x := 2;
y := 1;
writeln(zad1(x, y)); // ?
writeln(zad1(y, x)); // ?
writeln(x, y); // x=?, y=?
readln;
END.
5.8.
Funkcje rekurencyjne
Rekurencja polega na tym, że funkcja może wywoływać samą siebie. Może
to robić każda funkcja, ale jeśli nie ma w niej warunku zatrzymującego rekuren-
cję, to będzie się wywoływać w nieskończoność, tzn. aż do wyczerpania pamięci
na stosie. Gdy funkcja rekurencyjna wywołuje samą siebie, na stosie są już
zmienne lokalne będące własnością pierwszego wywołania tej funkcji. Kolejne
jej wywołanie umieszcza na stosie swoje własne zmienne lokalne.
Przykład 1. Procedura wywołująca siebie rekurencyjnie.
Program procedura_rek;
var k : Integer;
procedure inkrementacja(x : Integer);
begin
x := x+1;
if x < 4 then inkrementacja(x) else
writeln('Koniec wywoływania rekursyjnego dla x =',x);
writeln('Jestem ostatnim poleceniem na poziomie ',x);
end;
BEGIN
k := 0;
inkrementacja(k);
readln;
END.
Przykład 2. Rekurencyjne obliczanie silni.
program silnia_rekurencyjna;
function Rsilnia(liczba : Integer) : Cardinal;
begin
if liczba=0 then Rsilnia := 1
else Rsilnia := liczba*Rsilnia(liczba-1);
end;
var n : Byte;
91
BEGIN
writeln('Podaj n=');
readln(n);
writeln(' n! = ', Rsilnia(n));
readln;
END.
Przykład 3. Rekurencyjne obliczanie kolejnych liczb ciągu Fibonacciego.
program FibRekur; // F
0
=0, F
1
=1, F
n
= F
n-1
+ F
n-2
function Fib(n : Cardinal) : Cardinal;
begin
if n <= 1 then Result := n
else Result := Fib(n-1) + Fib(n-2);
end;
var i : Integer;
BEGIN
for i := 0 to 40 do writeln(i:4, ' : ', Fib(i));
readln;
END.
5.9.
Typ funkcyjny
Typ funkcyjny (proceduralny) deklarowany słowami kluczowymi
type oraz
procedure lub function stworzony został po to, aby traktować procedury lub
funkcje jako wartości pewnych zmiennych.
type MojaFunkcja = function (a, b : byte) : real;
W przedstawionym przykładzie wartość zmiennej typu proceduralnego
MojaFunkcja staje się funkcją realizującą zadania wybrane przez użytkownika.
Typy i zmienne proceduralne można również definiować w sekcji deklara-
cji zmiennych, np.:
var MojaFunkcja : function (a, b : byte) : real;
Przykład:
program typ_procedur2;
type TFunkcja = function(a,b : Integer) : Real;
function dodaj(skladnik1, skladnik2 : Integer) : Real;
begin
Result := skladnik1 + skladnik2;
end;
function odejmij(sklad1, sklad2 : Integer) : Real;
begin
Result := sklad1 - sklad2;
end;
92
function pomnoz(czynnik1, czynnik2 : Integer) : Real;
begin
Result := czynnik1 * czynnik2;
end;
function podziel(dzielna, dzielnik : Integer) : Real;
begin
if dzielnik<>0 then Result := dzielna / dzielnik;
end;
var
mFunkcja : TFunkcja;
x1, x2 : Integer;
zn : Char;
BEGIN
writeln('Podaj dwie liczby');
readln(x1,x2);
writeln('wybierz dzialanie: +dodawanie, -odejmownie,
* mnozenie,/ dzielenie');
repeat
readln(zn);
until zn in ['+', '-', '*', '/'];
case zn of
'+' : mFunkcja := @dodaj;
'-' : mFunkcja := @odejmij;
'*' : mFunkcja := @pomnoz;
'/' : mFunkcja := @podziel;
end;
writeln('wynik wynosi= ', mfunkcja(x1, x2));
readln;
END.
5.10.
Procedury kierujące działaniem programu
Wśród procedur standardowych modułu
System oraz SysUtils znajdują się
procedury kierujące działaniem programu, mianowicie:
•
Exit – procedura ta służy do przerwania bieżącego podprogramu (pro-
cedury lub funkcji) i powrotu do programu głównego. Jeśli jest wywoła-
na z programu głównego, to kończy jego działanie.
•
Halt (kod_wyjścia) – procedura ta powoduje natychmiastowe zakończe-
nie działania programu i przekazuje sterowanie do systemu operacyjne-
go. Procedura
Halt może być wywołana bez parametru lub z jednym pa-
rametrem typu
Integer, którego wartość jest zwracana systemowi opera-
93
cyjnemu jako kod wyjścia z programu. Użycie wywołania procedury
Halt w postaci Halt; albo Halt (0); oznacza normalne (bez błędu) za-
kończenie programu. Procedura
Halt znajduje zastosowanie przy wywo-
łaniach danego programu tekstowego z innego programu.
•
RunError – procedura ta powoduje przerwanie wykonywania programu
i wygenerowanie błędu jego wykonywania o podanym w nawiasie ko-
dzie (argument typu
Byte). Wywołanie RunError; oznacza, że kod błę-
du wynosi 0, natomiast
RunError (kod_błędu); zwraca kod_błędu.
•
Sleep
−
procedura ta jest wywoływana z jednym parametrem typu
Car-
dinal. Powoduje ona zatrzymanie programu na liczbę milisekund równą
wartości podanego parametru.
•
Abort
−
ta bezparametrowa procedura generuje tzw. cichy błąd, który
nie jest wyświetlany w postaci komunikatu. Procedura
Abort kończy
działanie programu bez informacji o błędzie.
Przykłady:
program Pr_exit;
var i : byte;
procedure zewnetrzna;
procedure wewnetrzna;
begin
writeln('To napisała procedura
wewn
ę
trzna');
exit;
writeln('a tego nigdy nie zo-
baczymy');
end;
begin
writeln('To napisała procedura ze-
wn
ę
trzna');
wewnetrzna;
//tu wyw. procedur
ę
wewnetrzn
ą
writeln('ten napis si
ę
pojawi,
kiedy wewn
ę
trzna przeka
ż
e sterowa-
nie do zewn
ę
trznej');
exit;
writeln('A ten napis ju
ż
si
ę
nie
pojawi');
program Pr_sleep_halt;
Uses SysUtils;
//W tym module jest procedura sleep
var
s : string;
I : byte;
BEGIN
s := 'Dziala opoznienie- (sleep)';
for i := 1 to length(s) do
begin
write(s[i]);
sleep(400);
end;
// readln;
writeln ('a teraz zadziała proce-
dura Halt, za 5 sekund');
sleep(5000);
Halt;
Writeln('Tego napisu nie zobaczy-
my')
END.
94
end;
BEGIN // program główny
writeln('wywołujemy procedur
ę
ze-
wn
ę
trzn
ą
');
zewnetrzna;
readln;
exit;
writeln('ten napis te
ż
si
ę
nie po-
jawi');
readln;
END.
5.11.
Kryteria stosowania procedur i funkcji
Podczas podejmowania decyzji o wyborze procedury czy funkcji do zapro-
gramowania podprogramu, warto kierować się następującymi wskazówkami:
•
jeśli podprogram ma zwracać więcej niż jedną wartość bądź modyfiko-
wać wartości parametrów aktualnych, należy użyć procedury,
•
jeśli podprogram ma zwracać dokładnie jedną wartość, najlepiej użyć
funkcji,
•
bardzo często procedurę można zastąpić równoważną funkcją i odwrot-
nie, wtedy należy wybrać ten sposób, który mamy lepiej opanowany,
•
zmienne mające w podprogramie charakter roboczy należy deklarować
jako zmienne lokalne,
•
bardziej bezpieczne i zalecane jest przekazywanie parametrów aktual-
nych do podprogramu przez wartość, gdyż są one w ten sposób chronio-
ne przed niepotrzebnymi zmianami (wszelkie zmiany, jeżeli będą w
podprogramie, zostaną dokonane na kopiach parametrów aktualnych),
•
jeśli parametr służy do komunikowania otoczeniu efektów wykonania
podprogramu, to musi być przekazywany przez zmienną,
•
jeśli zachodzi konieczność przekazania dużej struktury danych do pod-
programu, to należy unikać przekazywania jej przez wartość. Pozwoli to
na lepsze gospodarowanie pamięcią, ponieważ dla parametrów przeka-
zywanych przez wartość jest tworzona ich kopia na stosie.
95
5.12.
Moduły
Funkcje i procedury, napisane przez użytkownika, mogą być połączone
w jeden
moduł, z którego można korzystać w wielu aplikacjach.
Reguły tworzenia modułów są następujące:
•
nazwa pliku, w którym znajduje się moduł, powinna być taka sama, jak
nazwa modułu,
•
moduł składa się z części opisowej, implementacyjnej i inicjalizującej;
część inicjalizująca musi kończyć się słowem kluczowym
end i kropką,
•
stosowanie modułów powoduje, że właściwe programy są znacznie
krótsze, bardziej przejrzyste i czytelne,
•
dzięki temu, że moduły przechowywane są w postaci skompilowanej,
w trakcie kompilacji programu definicje zawarte w modułach są do pro-
gramu wynikowego dołączane, a zatem kompilacja przebiega szybciej.
Fragment modułu, umieszczony pomiędzy słowami
interface i implemen-
tation, jest tzw. częścią publiczną, a pomiędzy słowami implementation oraz
end jest tzw. częścią prywatną modułu.
W programach, w których zadeklarowano chęć korzystania z modułu, są
dostępne tylko definicje umieszczone w części publicznej, tzn.:
•
żaden typ, zmienna lub stała umieszczona w części prywatnej nie
jest dostępna w programie,
•
definicje funkcji i procedur, niezależnie od tego czy mają być dostępne
w programie czy nie, muszą być umieszczone w części prywatnej,
•
procedura lub funkcja staje się publiczna, gdy jej
nagłówek zostanie
umieszczony w części publicznej modułu.
Liczba modułów, z których może składać się program, jest praktycznie nie-
ograniczona. Jeśli jeden moduł korzysta z innego, to istotna jest kolejność nazw
na liście deklarowanych modułów. Moduł wykorzystywany powinien znajdować
się przed modułem wykorzystującym go.
Nazwy występujące w programie są nadrzędne w stosunku do tych wystę-
pujących w modułach. Przesłonięte identyfikatory z modułu są dostępne w pro-
gramie, jeśli są poprzedzone nazwą modułu i kropką.
Dołączanie modułu w programie odbywa się za
pomocą słowa kluczowego
uses według szablonu:
program z_wlasnym_modulem;
uses
SysUtils, System, Unit1 in 'Unit1.pas';
96
Unit Unit1
Interface
// część publiczna
Implementation
// część prywatna
End.
Przykład. Definiowanie własnego modułu.
Program przyklad_modulu;
uses SysUtils, modul1;
var a : Tab;
BEGIN
gener(A);
wypisz(A);
readln;
END.
Unit modul1;
Interface
const n = 2;
type Tab = array [1..N,1..N] of Integer;
procedure gener(var T : Tab);
procedure wypisz(T : Tab);
Implementation
procedure gener(var T : Tab);
var i, j : Byte;
Begin
for i:=1 to N do
for j:=1 to N do T[i,j] := random(10);
End;
procedure wypisz(T : Tab);
var i, j : Byte;
Begin
for i := 1 to N do
for j := 1 to N do
if j < N then write(T[i,j]:4,' ')
else writeln(T[i,j]:4)
End;
End. // Unit … End.
97
5.13.
Zadania
Zad. 5.1. Napisać funkcję zamiany liczby dziesiętnej na szesnastkową.
Zad. 5.2. Napisać funkcję zamiany liczby szesnastkowej na dziesiętną.
Zad. 5.3. Napisać funkcję obliczania przybliżonej wartości e
x
według sumy:
!
...
!
2
!
1
1
e
2
n
x
x
x
n
x
+
+
+
+
≈
, x-dowolne.
Zad. 5.4. Napisać funkcję obliczania przybliżonej wartości cos(x) według sumy:
)!
2
(
)
1
...(
!
6
!
4
!
2
1
cos
2
6
4
2
n
x
x
x
x
x
n
n
−
+
−
+
−
=
, x-dowolne.
Zad. 5.5. Napisać program i rekurencyjną funkcję odwracania ciągu liter. Wśród
parametrów formalnych funkcji powinny wystąpić: napis typu
string
oraz indeks typu
Byte.
Zad. 5.6. Napisać program i rekurencyjną funkcję obliczania największego
wspólnego dzielnika NWD dwóch liczb.
Zad. 5.7. Napisać program obliczania największego wspólnego dzielnika n liczb.
Zad. 5.8. Napisać program i funkcję, która zwróci tablicę n-elementową typu
Integer (generowanie danych losowych). Argumentem wejściowym
jest rozmiar tablicy, czyli n.
Zad. 5.9. Napisać funkcję, która zwróci element o maksymalnej wartości bez-
względnej dla tablicy n-elementowej.
Zad. 5.10. Napisać program i funkcję, która wyznaczy element najbardziej zbli-
ż
ony do średniej arytmetycznej elementów tablicy n-elementowej typu
Double.
Zad. 5.11. Napisać funkcję Dwumian(n, k :
Cardinal) : wektor, gdzie type wek-
tor =
array of Cardinal, która zwróci wektor czynników powstałych
po pełnym skróceniu ułamka dla współczynnika dwumianowego
k
k
n
n
n
k
n
⋅
⋅
⋅
+
−
−
=
K
K
2
1
)
1
(
)
1
(
. Ponieważ
k
n
zawsze przyjmuje warto-
ś
ci całkowite, więc takie skrócenie jest możliwe. Na przykład, dla
12
=
n
i
5
=
k
, po skróceniu ułamka, otrzymamy iloczyn:
12
11
3
2
⋅
⋅
⋅
.
Uwaga: zadanie ma wiele poprawnych rozwiązań.
98
6.
Operacje na plikach
Plik jest logicznym modelem fizycznego zbioru danych. Fizyczne zbiory
danych mogą być wprowadzane i wyprowadzane przez urządzenia zewnętrzne
komputera takie jak: klawiatura, ekran, drukarka, dyski stałe, optyczne itp. Do-
stęp do poszczególnych elementów pliku odbywa się sekwencyjnie, tzn. w da-
nym momencie dostępny jest co najwyżej jeden element pliku. Następny ele-
ment może być dostępny dopiero po wykonaniu jakiejś operacji na poprzednim
elemencie. Kolejność elementów w pliku zależy od kolejności, w jakiej zostały
do niego zapisane. Liczba elementów pliku jest ograniczona pojemnością dysku.
Plik jest ciągiem elementów tego samego typu. Liczba elementów pliku
zależy od programu go zapisującego. Od tablicy plik różni się metodą dostępu
do poszczególnych elementów oraz tym, że może istnieć w pamięci dyskowej
komputera po zakończeniu działania programu (i po wyłączeniu zasilania).
Aby w programie było możliwe odczytywanie danych z pliku lub zapisy-
wanie danych do pliku, konieczne jest zdefiniowanie zmiennej plikowej odpo-
wiedniego typu i skojarzenie jej (przywiązanie) do konkretnego pliku na dysku.
Najlepiej najpierw zdefiniować typ plikowy, a potem zmienną plikową.
Zmienna plikowa (zmienna wskazująca, wskaźnik położenia) zawsze pokazuje
na jakieś miejsce w pliku. Zmienna plikowa zezwala na dostęp do każdego ele-
mentu pliku i pośredniczy we wszystkich operacjach na nim wykonywanych.
6.1.
Rodzaje plików
Możliwe są trzy sposoby definicji typu plikowego:
1)
type nazwa_typu_plikowego = file of typ_elementów_w_pliku;
Ten typ plikowy oznacza
plik zdefiniowany, ponieważ w sposób jawny podany
jest typ elementów w pliku. Nazwa typ_elementów_w_pliku może być dowol-
nym typem prostym, porządkowym, strukturalnym, wskaźnikowym lub zdefi-
niowanym przez użytkownika (np. rekordem). Plik zdefiniowany jest plikiem
binarnym, tzn. niezrozumiałym (nieczytelnym) po otwarciu go w edytorze tek-
stu. Przykłady deklaracji zmiennych plikowych typu zdefiniowanego:
type dane = file of Double;
var moj_plik : dane;
type MaleLiczby = file of Smallint;
var plik_temp : MaleLiczby;
99
2)
type nazwa_typu_plikowego = file;
Powyższa definicja oznacza, że typ elementów w pliku jest niezdefiniowany.
Pliki niezdefiniowane stosuje się w celu dostępu do fizycznych zbiorów dys-
kowych zgodnie z ich wewnętrznym formatem. Przykład:
type plik = file;
3)
type nazwa_typu_plikowego = TextFile; (albo Text)
W Lazarus FPC istnieje predefiniowany typ określający
plik tekstowy. Elemen-
tami pliku tekstowego są wiersze składające się ze znaków. Znaki te mogą two-
rzyć liczby, wyrazy, tabele danych itp. Każdy wiersz zakończony jest parą zna-
ków CR i LF (ang.
Carriage Return i Line Feed), których kody ASCII wynoszą
13 i 10 w systemie dziesiętnym oraz D i A w systemie szesnastkowym.
Przykład deklaracji pliku tekstowego:
var plik_1 : TextFile;
6.2.
Etapy przetwarzania pliku
Na
przetwarzanie pliku dowolnego rodzaju składają się następujące czynności:
1.
Deklaracja zmiennej plikowej.
2.
Skojarzenie zmiennej plikowej z fizycznym zbiorem danych.
3.
Otwarcie pliku.
4.
Wykonanie operacji na pliku (odczyt danych lub zapis).
5.
Zamknięcie pliku.
1. Przykłady deklaracji zmiennych plikowych:
var plik1 : file of Integer; // plik zdefiniowany
plik2 : file of Byte; // plik zdefiniowany
plik3 :
TextFile; // plik tekstowy
plik4 :
file; // plik niezdefiniowany
type TZesp = record
re, im :
Extended;
end;
var plik5 : file of TZesp; // plik zdefiniowany
2. Do skojarzenia zmiennej plikowej z fizycznym zbiorem danych służy proce-
dura standardowa
AssignFile (zmienna_plikowa, łańcuch_znakowy);
Przykłady kojarzenia zmiennej plikowej z plikiem na dysku:
AssignFile (plik1, ’wyniki.bin’);
AssignFile (plik2, ’c:\Dane\dane’); // plik bez rozszerzenia
AssignFile (plik3, ’osoby.txt’);
100
3. W zależności od kierunku przesyłania danych, stosowane są trzy sposoby
otwierania lub tworzenia pliku:
a) otwarcie istniejącego pliku do odczytu
−
zmienna plikowa pokazuje na pierw-
szy element w pliku. Przykłady:
Reset (plik1); Reset (plik2);
b) otwarcie istniejącego pliku tekstowego w celu dopisywania tekstu na końcu
pliku. Zmienna plikowa pokazuje na miejsce za ostatnim znakiem w pliku tek-
stowym. Przykłady:
Append (plik1); Append (plik2); Procedurę Append można
stosować tylko do plików tekstowych.
c) utworzenie nowego pliku, w którym będą zapisywane dane. Jeżeli istniał plik
o nazwie takiej samej, jak ta, którą chcemy użyć, to zostanie zniszczony. Przy-
kłady:
Rewrite (plik1), Rewrite (plik4, 10); Drugie wywołanie jest stosowane
tylko do plików niezdefiniowanych, pierwsze
−
do wszystkich. Liczba 10 ozna-
cza, że każdy zapis do pliku niezdefiniowanego będzie składał się z 10 bajtów.
Zapis
Rewrite (plik4) oznacza, że domyślny rozmiar zapisu wynosi 128 bajtów.
4. Przetwarzanie pliku oznacza odczytywanie danych znajdujących się w pliku
albo zapisywanie danych do niego.
Zapisywanie danych do plików tekstowych odbywa się za pomocą procedur
Write i Writeln. Przykłady:
Write (plik3, zmienna); Write (plik3, zmienna1, zmienna2);
Writeln (plik3, a, ’+’, b, ’=’, a+b);
Zapisywanie danych do plików zdefiniowanych odbywa się za pomocą pro-
cedury
Write. Przykłady: Write (plik1, liczba); Write (plik2, a, b, c);
W tym przypadku zmienna liczba musi być typu
Integer; natomiast zmienne a, b
i c muszą być typu
Byte.
Zapisywanie danych do plików niezdefiniowanych odbywa się za pomocą
procedury
BlockWrite. Przykłady:
BlockWrite (plik4, bufor, licznik); Zmienna bufor jest zmienną dowolnego typu.
Zmienna licznik jest typu
Integer i określa liczbę zapisów do pliku niezdefinio-
wanego, które chcemy wykonać. Bajty do zapisu są pobierane po kolei z bufora.
Liczbę bajtów dla jednego zapisu określa się wcześniej w procedurze
Rewrite.
Jeżeli rzeczywista liczba wykonanych zapisów na dysk jest różna od wartości
licznika, to wystąpi błąd. Jeśli w wywołaniu funkcji
BlockWrite użyjemy po-
mocniczej zmiennej faktyczny_licznik w postaci:
BlockWrite (plik4, bufor, licz-
nik, faktyczny_licznik); to błąd nie wystąpi, a liczbę udanych zapisów będzie
można odczytać w zmiennej faktyczny_licznik.
101
Odczyt danych z plików zdefiniowanych odbywa się za pomocą procedury
Read. Przykłady: Read (plik1, liczba); Read (plik2, a, b, c);
W tym przypadku zmienna liczba musi być typu
Integer; natomiast zmienne a, b
i c muszą być typu
Byte.
Odczyt danych z plików niezdefiniowanych odbywa się za pomocą proce-
dury
BlockRead. Przykłady:
BlockRead (plik4, bufor, licznik); albo
BlockRead (plik4, bufor, licznik, faktyczny_licznik);
Znaczenie zmiennych bufor, licznik i faktyczny_licznik jest takie samo, jak dla
procedury
BlockWrite.
Odczyt danych z plików tekstowych odbywa się za pomocą procedur
Read
i
Readln. Przykłady: Read (plik3, znak); zmienna znak może być typu np. Char.
Readln (plik3, a, b); zmienne a i b mogą być typu np. Integer i Extended.
5. Po zakończeniu przetwarzania pliku należy go zamknąć procedurą CloseFile.
Przykłady:
CloseFile (plik1); CloseFile (plik2);
W przetwarzaniu plików tekstowych wykorzystywane są następujące funk-
cje standardowe, zwracające
True lub False:
Eof (zmienna_plikowa)
−
funkcja ta zwraca wartość
True, jeżeli zmienna_
plikowa pokazuje na miejsce za ostatnim znakiem w pliku lub gdy plik nie za-
wiera żadnych znaków.
EoLn (zmienna_plikowa)
−
funkcja ta zwraca wartość
True, jeżeli zmien-
na_plikowa pokazuje na znak końca wiersza (CR) lub gdy
Eof (zmien-
na_plikowa) =
True.
SeekEof (zmienna_plikowa)
−
funkcja ta zwraca wartość
True, jeżeli
zmienna_ plikowa pokazuje na miejsce za ostatnim znakiem w pliku lub gdy plik
nie zawiera żadnych znaków, lub gdy jedynymi znakami do końca pliku są znaki
odstępu (spacje), tabulacji lub CR i LF.
SeekEoLn (zmienna_plikowa)
−
funkcja ta zwraca wartość
True, jeżeli
zmienna_plikowa pokazuje na znak końca wiersza (CR) lub gdy
Eof (zmien-
na_plikowa) =
True, lub gdy jedynymi znakami do końca wiersza są znaki od-
stępu (spacje), tabulacji lub CR i LF.
102
6.3.
Przykładowe programy działające na plikach
Przykład 1. Zapis liczb rzeczywistych do pliku zdefiniowanego.
program Pliki1;
var
f : file of Real;
x : Real;
BEGIN
AssignFile(f, 'plik.bin');
Rewrite(f);
x := 3.1415926;
write(f,x);
x := -1.8;
write(f,x);
write(f,x,x); // writeln(f,x,x); <-- niedozwolone
CloseFile(f);
readln;
END.
Przykład 2. Odczyt liczb rzeczywistych z pliku zdefiniowanego z przykładu 1.
program Pliki2;
var
f : file of Real;
x : Real;
BEGIN
AssignFile(f, 'plik.bin');
Reset(f);
while not EoF(f) do
begin
read(f, x);
writeln(x);
end;
CloseFile(f);
readln;
END.
Przykład 3. Zapis dwóch liczb rzeczywistych i całkowitej do pliku tekstowego.
program Pliki3;
var
f : TextFile;
x : Real;
103
BEGIN
AssignFile(f, 'plik.txt');
Rewrite(f);
x := 3.14;
writeln(f, x, ' ', -x, ' ', 2+2);
writeln(f, x, ' ', -x:8:3, ' ', 5);
CloseFile(f);
readln;
END.
Przykład 4. Odczyt dwóch liczb rzeczywistych i całkowitej z pliku tekstowego.
program Pliki4; //odczyt danych z pliku z przykładu 3.
var
f : TextFile;
x, y : Real;
i : Integer;
BEGIN
AssignFile(f, 'plik.txt');
Reset(f);
while not EoF(f) do
begin
readln(f,x,y,i);
writeln('x=', x, ' y=', y, ' i=', i);
end;
CloseFile(f);
readln;
END.
Przykład 5. Zapis liczb całkowitych z tablicy do pliku tekstowego i ich odczyt.
program wewy1;
const N = 5;
var
we, wy : TextFile;
zn : Char;
A, B : array [1..N] of Byte;
i : Byte;
BEGIN
for i := 1 to N do A[i] := 10*i;
AssignFile(we, 'Tplik.txt');
Rewrite(we);
for i := 1 to N do Writeln(we, A[i]);
CloseFile(we);
104
AssignFile(wy, 'Tplik.txt');
Reset(wy);
i := 1;
while (not EoF(wy)) and (i<=N) do
begin
readln(wy, B[i]);
write(B[i]);
i := i+1;
end;
CloseFile(wy);
readln;
END.
Przykład 6. Odczyt łańcuchów znakowych z pliku tekstowego.
program wewy2;
var
we1 : TextFile;
napis : string;
BEGIN
AssignFile(we1, 'wewy2.lpr');
Reset(we1);
while not EoF(we1) do
begin
readln(we1, napis);
if length(napis)<>0 then
if napis[1] in ['A'..'Z'] then writeln(napis)
// wypisuje na ekranie tylko te wiersze,
// które zaczynaj
ą
si
ę
od wielkiej litery
end;
CloseFile(we1);
readln;
END.
6.4.
Zadania
Zad. 6.1. W pliku tekstowym liczby.txt są zapisane liczby rzeczywiste – po dwie
w każdym wierszu, oddzielone białymi znakami. Program ma czytać te pa-
ry liczb. Jeśli ich iloczyn będzie większy od zera, to program ma zapisać go
do pliku tekstowego
dod.txt. Jeśli ich iloczyn będzie mniejszy od zera, to
program ma go zapisać do pliku
ujem.txt.
Zad. 6.2. W pliku tekstowym
we1.txt znajduje się kilka wierszy tekstu. Napisać
program zawierający funkcję Parzystosc_E
(s : string), która sprawdzi, czy
w danym wierszu jest parzysta liczba znaków ’e’ i ’E’. Funkcja ma zwracać
105
wartość
True, jeżeli liczba znaków ’e’ i ’E’ jest parzysta (zero też jest
parzyste) oraz ma zwracać wartość
False w przeciwnym wypadku.
Argumentem wejściowym funkcji jest wiersz pliku. Wynik działania
funkcji przedstawić na ekranie.
Zad. 6.3. Program czyta wiersze pliku tekstowego
we6.txt. Za każdym razem, po
napotkaniu wiersza, którego pierwszym znakiem jest 'A'..'Z', rozpoczyna
łączenie (sklejanie) kolejnych wierszy, rozdzielając je spacją. Sklejony
wiersz jest zapisywany do pliku
wy6.txt, przy czym na początku, w nawia-
sach, jest wpisywana liczba jego znaków.
Zad. 6.4. Program ma czytać plik tekstowy
zad4.lpr
−
wiersz po wierszu
−
i jeśli
w wierszu znajdzie ciąg dwóch ukośników '//', to zapisze dalszy ciąg tego
wiersza do pliku tekstowego
komentarze.txt oraz wypisze na ekranie.
Zad. 6.5. Napisać program Dekomentator, który wejściowy plik tekstowy,
zawierający kod źródłowy programu w Lazarus FPC, przetworzy tak, że
usunie z niego wszystkie możliwe komentarze (jedno i wielo-liniowe),
w tym także zagnieżdżone. Uwaga na dyrektywy kompilatora!
Zad. 6.6. W kolejnych wierszach pliku tekstowego
we5.txt znajduje się albo
liczba naturalna, albo jakiś napis. Wiersz z liczbą naturalną tym różni się od
napisu, że występują w nim wyłącznie cyfry '0'..'9' oraz znaki ’+’, ’
−
’.
Napisać program, który odczyta kolejne wiersze wspomnianego pliku
i zapisze do pliku tekstowego
wy8.txt wyłącznie wiersze zawierające
napisy. Trzeba zaprogramować funkcję
JestLiczba zwracającą dla danego
wiersza wartość
True, jeśli zawiera on liczbę i False, jeśli zawiera on napis.
Zad. 6.7. Napisać program, który wczyta plik tekstowy
we7.txt i sprawdzi, ile
zdań jest w tym pliku. Zdanie zaczyna się wielką literą i kończy się kropką,
pytajnikiem lub wykrzyknikiem.
106
7.
Wskaźniki
Zmienne typów prostych i strukturalnych istnieją przez cały czas wykony-
wania tej części programu, w której zostały zadeklarowane. Są to tzw.
zmienne
statyczne.
Zmienne dynamiczne natomiast reprezentują obiekty, dla których pamięć
jest przydzielana i zwalniana na żądanie, w trakcie pracy programu. Zmienna
dynamiczna nie posiada własnego identyfikatora, a odwołanie do niej następuje
za pomocą zmiennej typu wskaźnikowego, w której pamiętany jest adres w pa-
mięci, gdzie ta zmienna dynamiczna się znajduje.
7.1.
Definicja zmiennej wskaźnikowej
Definicja zmiennej wskaźnikowej (w skrócie
−
wskaźnika) określa, jak in-
terpretować zawartość pamięci pod adresem pokazywanym przez wskaźnik.
Na przykład definicja wsk : ^
Byte; oznacza, że obszar pamięci, pokazywany
przez wskaźnik wsk, zawiera liczbę typu
Byte.
Przykłady deklaracji wskaźników:
var
wsk_int : ^
Integer; {wskaźnik na zmienną typu Integer}
wsk_char : ^
Char; {wskaźnik na zmienną typu Char}
wsk_real : ^
Single; {wskaźnik na zmienną typu Single}
type
tab =
array [1..10] of Integer; {definicja typu tablicowego}
wsk = ^ tab; {definicja typu będącego wskaźnikiem do tablicy}
var
wsk_tab : ^ tab; {definicja zmiennej wskazującej na tablicę}
// wsk_tab : wsk;
←
to samo, co wsk_tab : ^ tab;
tab_wsk :
array [1..10] of ^ Integer; {tablica zawierająca 10 wskaźników
do liczb całkowitych typu
Integer}
var p : Pointer; {wskaźnik na zmienną dowolnego typu}
Zmienne typu
Pointer nie pokazują na dane żadnego konkretnego typu.
Zawartość zmiennej typu
Pointer można podstawić do zmiennej dowolnego
typu wskaźnikowego i na odwrót.
Uwagi dotyczące wskaźników:
•
deklaracja zmiennej wskaźnikowej nie jest jednoznaczna z utworzeniem
zmiennej wskazywanej,
107
•
każda zmienna wskaźnikowa musi być przed użyciem zainicjalizowana,
•
nazwa zmiennej odwołuje się do obszaru pamięci o określonym adresie,
•
adres pamięci, pod którym znajduje się zmienna, można uzyskać za po-
mocą operatora @ , np. @ zmienna.
•
ten sam wskaźnik może odwoływać się do różnych miejsc w pamięci,
ale nie równocześnie,
•
wskaźnik służący do pokazywania na obiekty jednego typu nie może
(zazwyczaj) pokazywać na obiekty innego typu,
•
za pomocą wskaźnika uzyskujemy dostęp do pewnego obszaru pamięci
i możemy modyfikować jego zawartość,
•
wskaźniki mogą być elementami rekordów,
•
dozwolone jest używanie wskaźników do rekordów.
Zmiennej wskaźnikowej można nadać wartość początkową na kilka sposo-
bów, mianowicie poprzez:
•
przypisanie standardowej wartości stałej
nil,
•
przypisanie adresu zmiennej dynamicznej utworzonej za pomocą proce-
dury
New lub GetMem,
•
przypisanie adresu istniejącej zmiennej, której typ jest taki, na jaki po-
kazuje wskaźnik,
•
przypisanie wartości innej zainicjalizowanej zmiennej wskaźnikowej.
7.2.
Procedury dynamicznego rezerwowania i zwalniania pamięci
Wskaźnik pokazuje na pewien obszar w pamięci i w związku z tym:
•
może to być obszar przechowujący zmienną, a więc już zarezerwowany,
•
można zarezerwować (zaalokować) nowy obszar dla wskaźnika za po-
mocą procedury
New,
•
jeśli obszar pamięci jest już niepotrzebny, to należy go zwolnić za po-
mocą procedury
Dispose,
•
obiekty utworzone za pomocą operatora
New istnieją dopóty, dopóki nie
zostaną zlikwidowane procedurą
Dispose,
•
do obiektów utworzonych za pomocą procedury
New można odwoływać
się
tylko za pomocą wskaźnika,
•
obiekty tworzone w ten sposób są dynamiczne i nie są wstępnie inicjali-
zowane zerami.
Standardowa stała
nil oznacza, że zmienna wskaźnikowa na nic nie pokazu-
je;
nil jest to tzw. adres „zero”, czyli żaden konkretny adres. Przykład:
var wsk : ^ Integer; . . . wsk := nil; // wsk pokazuje na adres zerowy
108
Rys. 7.1. Wskaźnik z adresem pustym (nil)
Przypisanie wskaźnikowi wartości
nil jest wykorzystywane do zaznaczenia,
ż
e wskaźnik nie pokazuje na nic konkretnego. Bez takiego przypisania wskaźnik
pokazuje na jakiś losowy adres w pamięci.
Powstanie zmiennej dynamicznej w : ^typ1, za pomocą procedury
New, od-
bywa się w kilku etapach:
•
utworzenie nowej zmiennej dynamicznej typu typ1, która w tym mo-
mencie jest nieokreślona
−
nadawana jest jej automatycznie nazwa w^.
•
utworzenie nowego wskaźnika w typu ^typ1, który wskazuje na zmien-
ną typu typ1.
•
nadanie wartości tego wskaźnika zmiennej, dla której procedura
New
została wywołana (czyli nadanie zmiennej dynamicznej w
^ adresu pa-
mięci przeznaczonego dla zmiennej typu typ1).
•
zapisanie do obszaru pamięci, na który wskazuje wskaźnik w, konkret-
nej wartości jakiegoś wyrażenia.
var w : ^typ1;
New(w);
w^ := wyrażenie;
Rys. 7.2. Wskaźnik W pokazujący obszar pamięci W^
Procedura
GetMem różni się od procedury New tym, że wielkość zarezer-
wowanego miejsca w pamięci określana jest dopiero w czasie działania progra-
mu i sami możemy zdecydować o rozmiarze potrzebnej pamięci, podając drugi
parametr tej procedury, tzn. liczbę bajtów:
wsk : ^typ;
GetMem (wsk, liczba_bajtów);
Przykład użycia procedury
GetMem:
type tab = array [1..20] of Integer;
wsk_tab = ^tab;
109
var
wpom : wsk_tab;
…
GetMem (wpom, 20*SizeOf (Integer)); {utworzenie zmiennej dynamicznej
o 20 elementach typu
Integer}
wpom^ [ 1] := 5; { poprawne odwołanie do pierwszego elementu }
wpom^ [
−
2] := 10; { błąd zakresu zgłaszany przez kompilator }
wpom^ [25] :=
−
7; { błąd zakresu niezgłaszany przez kompilator (25>20) }
Funkcja standardowa
Addr (x) zwraca wskaźnik typu Pointer, zawierający
adres miejsca, gdzie zapamiętane jest x, np. w :=
Addr (x);
Operator adresu @ służy do odczytania adresu zmiennej, na przykład:
var x : typ;
w : ^typ;
…
x := 12;
w := @ x; // wtedy w^ = x = 12
Przykład 1. Wskaźnik na łańcuch znakowy.
program wsk1;
type TDane = string[60];
var
w1 : ^TDane;
BEGIN
New(w1);
w1^ := 'Cos tam';
Writeln(w1^);
w1^[2] := 'U'; //podmiana litery 'o' na 'U'
Writeln(w1^);
Dispose(w1);
readln;
END.
Przykład 2. Dwa wskaźniki na ten sam łańcuch znakowy.
program wsk2;
type TDane = string[60];
var w1, w2 : ^TDane;
BEGIN
New(w1);
w1^ := 'Cos tam';
110
w2 := w1;
w2^ := 'Cos innego tam jest';
writeln(w1^);
writeln(w2^);
dispose(w2);
readln;
END.
Przykład 3.
Użycie zmiennych wskaźnikowych i dynamicznych.
program wsk3;
type
wsk_int = ^Integer;
typDanych = record
imie, nazwisko : string;
end;
wsk_rek = ^typDanych;
var
x1, x2 : wsk_int;
r1 : wsk_rek;
BEGIN
New(r1); // zmienna dynamiczna typu rekordowego
writeln('Podaj imi
ę
i nazwisko: ');
readln(r1^.imie, r1^.nazwisko);
with r1^ do writeln(imie, nazwisko);
dispose(r1);
New(x1); // zmienna dynamiczna typu integer
New(x2);
x1^ := 5;
x2^ := 10;
writeln('x1^=', x1^);
x2^ := x1^; // przepisanie zawarto
ś
ci
x2 := x1; // przepisanie adresu
writeln('x1^=', x1^);
writeln('x2^=', x2^);
dispose(x1);
// dispose(x2); // nie mo
ż
na zwolni
ć
dwa razy
readln;
END.
Zwalnianie pamięci przydzielonej wskaźnikowi w sposób dynamiczny od-
bywa się za pomocą procedur
Dispose i FreeMem. Procedury te umożliwiają
powtórne wykorzystanie obszarów pamięci oddawanych przez niepotrzebne już
zmienne dynamiczne.
111
przydzielenie pamięci:
zwolnienie pamięci:
New (p);
Dispose (p);
GetMem (p, liczba_bajtów);
FreeMem (p, liczba_bajtów);
7.3.
Wskaźniki i tablice
Wskaźniki są przydatne do pracy z tablicami. Zapis w := @tab[3]; powo-
duje, że wskaźnik w ustawia się na elemencie o indeksie 3 tablicy tab.
Nazwa tablicy jest równocześnie adresem jej pierwszego elementu, zatem
przypisanie w := @tab; jest równoważne przypisaniu w = @tab[pocz], jeśli
pocz jest pierwszym elementem tablicy tab.
Przykład 1. Tablica wskaźników do łańcuchów znakowych.
program wsk4;
type TDane = string[30];
const MaxN = 5;
var A : array [0..MaxN-1] of ^TDane; // tablica wsk.
i : 0..MaxN;
BEGIN //
↓
tablica pustych wska
ź
ników
for i := 0 to MaxN-1 do A[i] := nil;
New(A[2]);
A[2]^ := 'NapisNr1'; //dodawanie danych do tablicy
New(A[4]);
A[4]^ := 'NapisNr2';
for i := 0 to MaxN-1 do
if A[i] <> nil then
begin
writeln('nr: ', i, ' ', A[i]^);
dispose(A[i]); //usuwanie danych z tablicy
// A[i] := nil;
end;
readln;
END.
Przyklad 2. Wskaźnik do tablicy statycznej.
program wsk5;
const MaxN = 5;
type Tab = array [1..MaxN] of Byte;
wskTab = ^Tab;
var i : Byte;
B : Tab; //tablica statyczna
112
w : wskTab;
BEGIN //
↓
wypełnienie tablicy statycznej liczbami
for i := 1 to MaxN do B[i]:=i;
w := @B; // ustawienie wska
ź
nika na tablic
ę
writeln(w^[1]); // writeln(B[1]);
inc(w); // inkrementacja adresu
writeln(w^[2]); // poza adres całej tablicy
w := @B[3]; // wska
ź
nik pokazuje na trzeci element
writeln(w^[1]);
readln;
END.
Przykład 3. Wskaźniki do tablicy dynamicznej.
program wsk6;
const N = 5;
type Tab = array [1..N] of Byte;
wskTab = ^Tab;
var i : Byte;
k : ^Byte;
w, x : wskTab;
BEGIN
New(w); // utworzenie dynamicznej tablicy
for i := 1 to N do w^[i] := i; // wpisanie danych
writeln('Pierwszy =', w^[1]);
writeln('Drugi =', w^[2]);
writeln('N-ty =', w^[N]);
x := w;
x^[N] := 115;
for i := 1 to N do write(x^[i]:5);
writeln;
k := @w;
writeln(k^);
k := @w[1];// pierwszy element tablicy
writeln(k^);
Dispose(w);//zwolnienie pami
ę
ci przydzielonej tablicy
readln;
END.
113
7.4.
Operacje arytmetyczne na wskaźnikach
Ponieważ wskaźniki przechowują adresy, które są liczbami, to dopuszczal-
ne są niektóre operacje arytmetyczne na wskaźnikach:
•
dodawanie do
−
i
odejmowanie od wskaźników liczb naturalnych, za
pomocą procedur inkrementacji
Inc i dekrementacji Dec, powoduje
przesuwanie wskaźników w kierunku wyższych lub niższych adresów.
Ponieważ wskaźnik pokazuje na obiekt pewnego typu i znany
jest rozmiar tego typu, więc wiadomo o ile bajtów trzeba prze-
sunąć wskaźnik.
Operacje zwiększania i zmniejszania wskaźników mają istotne
zastosowanie w stosunku do elementów tablic. Jeżeli np.
zmienna wskaźnikowa zw wskazuje na piąty element tablicy, to
po
Dec(zw) będzie wskazywać na czwarty element, niezależnie
od typu elementów tablicy.
Używając procedur
Inc i Dec trzeba uważać, żeby nie wyjść ze
wskazaniem poza zakres tablicy.
Zwiększenie lub zmniejszenie zmiennej wskaźnikowej o okre-
ś
lonym typie (ale nie
Pointer) też odbywa się za pomocą proce-
dur
Inc i Dec. Na przykład: x : ^ Double; Inc (x, 2) spowoduje
zwiększenie wskaźnika x o 2*8 = 16 bajtów, natomiast k : ^
In-
teger; Inc (k, 3) spowoduje zwiększenie k o 3*4 = 12 bajtów.
•
odejmowanie dwóch wskaźników pokazujących na tę samą tablicę
−
w
wyniku dostajemy liczbę elementów tablicy dzielących elementy poka-
zywane przez oba wskaźniki. Liczba ta może być ujemna lub dodatnia.
•
porównywanie wskaźników
−
wskaźniki można ze sobą porównywać
za pomocą operatorów porównania: =,
<>, <, >, <=, >=,
równość wskaźników oznacza, że pokazują one na ten sam obiekt,
wskaźnik, który jest mniejszy, pokazuje na element o niższym ad-
resie (w tablicy – na element o niższym indeksie).
Jeżeli w oraz x są zmiennymi wskaźnikowymi, to porównanie adresów
przechowywanych w tych wskaźnikach przeprowadza się tak: w < x, natomiast
porównanie zawartości komórek pamięci odbywa się tak: w^ < x^.
114
8.
Struktury dynamiczne
W wielu algorytmach pojawia się potrzeba wykorzystania struktur danych,
umożliwiających w sposób dynamiczny wstawianie i pobieranie danych. Naj-
prostszymi taki strukturami są
kolejka, lista i stos.
8.1.
Kolejka
Kolejka to rodzaj listy jednokierunkowej, do której można dopisać element
tylko na końcu, a usunąć tylko element znajdujący się na początku.
Kolejka realizuje strategię wstawiania/pobierania, opartą na zasadzie pierw-
szy wszedł, pierwszy wyjdzie. Kolejka zachowuje kolejność wstawianych ele-
mentów.
Operacje, które wykonuje się na kolejce to: wstawianie elementu, pobranie
elementu, usunięcie elementu, usunięcie całej kolejki, wyświetlenie zawartości
kolejki oraz sprawdzenie, czy kolejka jest pusta.
Rys. 8.1. Schemat blokowy kolejki
8.2.
Lista jednokierunkowa
Lista jednokierunkowa jest zbiorem elementów zwanych węzłami, z któ-
rych każdy jest zwykle rekordem i składa się z dwóch części: pola danych i pola
wskaźnikowego, wykorzystywanego jako łącznik z następnym elementem listy.
type
wsk = ^ skladnik;
skladnik =
record
dane : typ;
next : wsk
end;
115
Dostęp do listy odbywa się poprzez wskaźnik, który zawiera adres pierw-
szego elementu na liście. Wskaźnik ten nazywany jest początkiem bądź korze-
niem listy. Każdy następny składnik listy jest dostępny poprzez składową zawie-
rającą adres w składniku poprzednim.
Rys. 8.2. Schemat blokowy listy
Przykład tworzenia nowej listy:
type
wsk = ^ skladnik;
skladnik = record
dane : typ;
next : wsk
end;
procedure tworz_liste;
var zn : Char;
begin
pocz := nil;
repeat
New(wsk);
write('Podaj kolejny
element listy')
readln(wsk^.dane);
wsk^.next := pocz;
pocz := wsk;
write('Czy kontynuowa
ć
?');
readln(zn)
until zn <> 't';
end;
Najważniejsze operacje dotyczące wszystkich typów list to:
•
dopisywanie nowego elementu:
kiedy lista jest pusta,
na jej początku,
na jej końcu,
w dowolnym miejscu wewnątrz listy.
116
•
usuwanie elementu:
znajdującego się na początku,
znajdującego się na końcu,
w dowolnym miejscu wewnątrz listy.
•
wyświetlanie listy.
Aby możliwe było wykonanie operacji wstawiania oraz usuwania nowych
elementów, lista musi być posortowana według określonego klucza. Kluczem do
sortowania może być np. pole nazwisko, jeśli mamy do czynienia z listą osób.
Nazwiska typu łańcuchowego możemy łatwo porównywać alfabetycznie.
Przykład. Program tworzy listę dynamiczną, ale bez sortowania. Polem danych
jest łańcuch 60-znakowy. Program zawiera procedury: dopisywania elementu na
początku listy, wypisywania całej listy i usuwania listy z pamięci.
program Lista1;
type
TDane = string[60]; // ła
ń
cuch 60-znakowy
PElem = ^TElem;
TElem = record
Dane : TDane; // pole danych
Nast : PElem; // wska
ź
nik na nast
ę
pny elem.
end;
TLista = PElem; // wska
ź
nik na pocz
ą
tek listy
procedure Inicjalizuj(var Lista : TLista);
begin
Lista := nil;
end;
procedure Dopisz(var Lista : TLista; D : TDane);
var w : PElem;
begin
w := Lista;
New(Lista);
Lista^.Dane := D;
Lista^.Nast := w;
end;
procedure WypiszWszystko(Lista : TLista);
var w : PElem;
117
begin
w := Lista;
while w <> nil do
begin
writeln(w^.Dane);
w := w^.Nast;
end;
end;
procedure UsunWszystko(var Lista : TLista);
var w : PElem;
begin
while Lista <> nil do
begin
w := Lista;
Lista := Lista^.Nast;
Dispose(w);
end;
end;
var Lst : TLista;
BEGIN
Inicjalizuj(Lst);
Dopisz(Lst, 'pierwszy');
Dopisz(Lst, 'drugi');
Dopisz(Lst, 'trzeci');
WypiszWszystko(Lst);
UsunWszystko(Lst);
readln;
END.
8.3.
Stos jako dynamiczna struktura danych
Stos jest dynamiczną strukturą danych, która umożliwia wstawianie ele-
mentów i ich pobieranie tylko z początku, zwanego wierzchołkiem stosu. Stos
realizuje strategię wstawiania/pobierania LIFO (Last In-First Out), czyli ostatni
wszedł, pierwszy wyjdzie. Stos odwraca kolejność wstawianych elementów.
Operacje wykonywane na stosie to:
•
wstawienie elementu na stos (ang. push),
•
zdjęcie elementu ze stosu (ang. pop),
•
sprawdzenie, czy stos jest pusty (ang. empty),
•
odczytanie elementu na szczycie (ang. top),
•
inicjalizacja stosu (ang. init),
118
•
usunięcie całego stosu (ang. clear),
•
wyświetlenie zawartości stosu (ang. display).
Przykład. Program wykonuje operacje arytmetyczne na elementach stosu. Ele-
mentami stosu są tablice kwadratowe. Zaprogramowane operacje to: wkładanie
tablicy na stos, zdejmowania tablicy ze stosu oraz dodawanie, odejmowanie
i mnożenie dwóch tablic znajdujących się na szczycie stosu. W programie głów-
nym zakodowano operacje: C := A+B, C := A
−
B, C := A*(B+C).
Program Stos;
const NMax = 2;
type float = Double;
TArr = array [0..NMax-1, 0..NMax-1] of float;
PElement = ^TElement;
TElement = record
poprz : PElement;
X : TArr;
end;
TStos = PElement; {wska
ź
nik na wierzchołek stosu}
procedure NaStos(var St : TStos; _X : TArr);
{_X - dane nowego elementu stosu}
var w : PElement;
begin
New(w);
w^.X := _X;
w^.poprz := St;
St := w;
end;
procedure ZeStosu(var St : TStos; var _X : TArr);
{X - zawarto
ść
likwidowanego elementu}
var w : PElement;
begin
if (St=nil) then EXIT; {bł
ą
d!}
w := St;
_X := w^.X;
St := w^.poprz;
Dispose(w);
end;
119
procedure Dodaj(var St : TStos);
{Dodaje dwa szczytowe elementy stosu - zdejmuje pierw-
szy, a wynik umieszcza w wierzchołku stosu.}
var i, j : 0..NMax-1;
T : TArr;
begin
ZeStosu(St, T);
for i := 0 to NMax-1 do
for j := 0 to NMax-1 do
St^.X[i,j] := St^.X[i,j] + T[i,j];
end;
procedure Odejmij(var St : TStos);
{Odejmuje najwy
ż
szy od ni
ż
szego elementu stosu - zdej-
muje 1 element, a wynik umieszcza w wierzchołku stosu}
var i, j : 0..NMax-1;
T : TArr;
begin
ZeStosu(St, T);
for i := 0 to NMax-1 do
for j := 0 to NMax-1 do
St^.X[i,j] := St^.X[i,j] - T[i,j];
end;
procedure Mnoz(var St : TStos);
{Mno
ż
y elementy stosu i wynik umieszcza
w wierzchołku stosu}
var i, j, k : 0..NMax-1;
A {czynnik}, C {wynik} : TArr;
begin
ZeStosu(St, A); {drugi czynnik}
for i := 0 to NMax-1 do
for j := 0 to NMax-1 do
begin
C[i,j] := 0;
for k := 0 to NMax-1 do
C[i,j] := C[i,j] + St^.X[i,k]*A[k,j];
end;
ZeStosu(St, A); {zdejmuje ze stosu drugi czynnik}
NaStos(St, C); {wkłada iloczyn na stos}
end;
procedure Wyswietl(Nagl : string; A : TArr);
{wy
ś
wietla element wskazywany przez W,
na pocz
ą
tku wypisuje nagłówek w postaci ła
ń
cucha}
120
var i, j : 0..NMax-1;
begin
writeln(Nagl);
for i := 0 to NMax-1 do
begin
for j := 0 to NMax-1 do
write(A[i,j]:6:1, ' ');
writeln;
end;
writeln;
end;
procedure InicjalizujDowolnie(var A : TArr);
var i, j : 0..NMax-1;
begin
for i := 0 to NMax-1 do
for j := 0 to NMax-1 do
A[i,j] := random(10)-5.0;
end;
procedure Pause;
begin
write('Naci
ś
nij ENTER...':60);
readln;
end;
procedure TestStNil;
begin
if St<>nil then writeln('Program zawiera bł
ę
dy!');
end;
var St : TStos;
A, B, C : TArr;
BEGIN
writeln(#13#10,'Stos i odwrotna notacja polska':60);
St := nil;
Randomize;
InicjalizujDowolnie(A);
InicjalizujDowolnie(B);
Wyswietl('A:', A);
Wyswietl('B:', B);
NaStos(St, A);
NaStos(St, B);
Dodaj(St);
ZeStosu(St, C);
121
Wyswietl('C=A+B', C);
TestStNil; Pause;
NaStos(St, A);
NaStos(St, B);
Odejmij(St);
ZeStosu(St, C);
Wyswietl('C=A-B', C);
TestStNil; Pause;
NaStos(St, A);
NaStos(St, B);
Mnoz(St);
ZeStosu(St, C);
Wyswietl('C=A*B', C);
TestStNil; Pause;
NaStos(St, A);
NaStos(St, B);
NaStos(St, C);
Dodaj(St);
Mnoz(St);
ZeStosu(St,C);
Wyswietl('C=A*(B+C)', C);
TestStNil; Pause;
END.
8.4.
Zadania
Zad. 8.1. Zadeklarować tablicę 100 wskaźników do rekordów. W rekordzie
umieścić imię i nazwisko typu
string. Program ma czytać z klawiatury in-
deks komórki do wypełnienia. Dane wprowadzamy dopóty, dopóki poda-
ny indeks nie przekroczy zakresu indeksów tablicy. Ten sam indeks moż-
na podawać wielokrotnie. Na koniec program wypisuje wszystkie dane
oraz niszczy zmienne dynamiczne.
Zad. 8.2. Zadeklarować tablicę wskaźników do obiektów. Dane: tablica 100 ko-
mórek typu
class, w klasie: imię i nazwisko typu string[40] oraz kon-
struktor. Program ma czytać indeks komórki do wypełnienia. Dane poda-
jemy dopóty, dopóki indeks nie przekroczy zakresu indeksów tablicy.
Na koniec program wypisuje wszystkie dane i niszczy obiekty.
Zad. 8.3. Zaprogramować stos liczb zespolonych. W tym celu zdefiniować
wierzchołek St typu PStos oraz PStos = ^TStos; TZesp =
record re, im :
Real;
end; W programie uwzględnić następujące operacje:
a)
wkładanie na stos
−
procedure push(var St : PStos; D : TZesp);
b)
zdejmowanie ze stosu
−
function pop(var St : PStos) : TZesp;
122
c)
dodawanie, odejmowanie i mnożenie pary liczb z wierzchołka stosu
−
function dod(a,b : TZesp): TZesp; function odej(a, b : TZesp): TZesp;
W programie głównym zaprogramować obliczanie wyrażenia
c := a + (b
* c) w odwrotnej notacji polskiej (ONP).
Zad. 8.4. Zaprogramować stos tak, jak w zad. 8.3, ale użyć typu
class zamiast
record. Zdefiniować klasy: TZesp, TElemSt i TStos. W klasie TZesp,
oprócz konstruktora Create, ustawiającego pola re, im, zdefiniować
dodatkowy
constructor dod(a, b : TZesp); i podobnie
−
odejm i mnoz.
W tych konstruktorach należy także wywołać
inherited Create; oraz
(na końcu) a.Free; Uwaga: zmienna
var c : TZesp; jest wskaźnikiem do
obiektu, więc nie trzeba dla niej wywoływać konstruktora, ponieważ w in-
strukcji c := St.pop; przekażemy do c adres istniejącego obiektu (stworzo-
nego konstruktorem dod).
Zad. 8.5. Zaprogramować listę jednokierunkową na wskaźnikach. Dane: imię,
nazwisko i rok urodzenia. W programie uwzględnić operacje:
a) dodawania osoby, tak że nazwiska są ułożone rosnąco alfabetycznie,
b) usuwania osoby o określonym nazwisku,
c) wyświetlania całej listy,
d) usuwania całej listy.
Zad. 8.6. Zaprogramować listę jednokierunkową na klasach. W klasie zadekla-
rować: imię, nazwisko i wiek. Uwzględnić operacje:
a) dodawania osoby, tak że nazwiska są ułożone rosnąco alfabetycznie,
b) usuwania osoby o określonym nazwisku,
c) wyświetlania całej listy,
d) usuwania całej listy.
Zad. 8.7. Zaprogramować kolejkę na klasach. W klasie zadeklarować: imię, na-
zwisko oraz rok studiów. W programie uwzględnić operacje:
a) dodawania elementu na końcu kolejki,
b) usuwania pierwszego elementu kolejki,
c) wyświetlania całej kolejki,
d) usuwanie całej kolejki.
123
9.
Klasy
Klasa jest złożoną strukturą danych, rozbudowaną o funkcje składowe.
Klasa
jest wzorcem, tzn. typem dla swoich zmiennych, czyli obiektów. Obiek-
tem nazywamy konkretny egzemplarz danej klasy. Klasę tworzą: dane skła-
dowe (zmienne) oraz funkcje składowe (metody), w tym specjalne metody
−
konstruktory i destruktory.
9.1.
Zasady deklarowania klasy
Klasę deklaruje się przy użyciu słowa kluczowego
class, mianowicie:
type TNazwaKlasy = class … end;
W dobrym stylu jest nadawanie klasie nazwy z dużej litery T. Elementy
klasy są podzielone na sekcje: publiczną, prywatną i chronioną. Elementy te
mogą być deklarowane w dowolnej liczbie występujących po sobie sekcji wy-
dzielonych przez etykiety
public lub private.
Elementy klasy są domyślnie prywatne. Metod i zmiennych publicz-
nych klasy można używać wszędzie w programie, natomiast do metod i
zmiennych prywatnych dostęp mają tylko inne metody klasy. Ponadto:
•
klasa powinna być abstrakcyjnym typem danych, czyli takim, który raz
zaprojektowany może być używany przez innych programistów bez ko-
nieczności wgłębiania się w jego mechanizm,
•
sekcja publiczna deklaracji klasy nazywana jest interfejsem klasy. Za-
warte w niej metody, wraz z opisami ich parametrów, działanie i format
wyniku powinny dawać programiście minimum informacji o klasie,
•
według zasady abstrakcyjności danych, wszystkie dane składowe klasy
powinny znajdować się w sekcji prywatnej,
•
w sekcji publicznej należy umieszczać metody operujące na danych
prywatnych,
•
jeśli metoda publiczna wykonuje wiele obliczeń lub pewne z nich są
wykonywane przez wiele metod publicznych, to można je wyodrębnić w
postaci osobnej metody prywatnej. Są one wtedy wyłącznie na użytek
implementacji klasy.
•
implementacja klasy powinna być zaprojektowana przez programistę w
sposób, który daje mu możliwość rozbudowania i udoskonalania działa-
124
nia funkcji jej składowych, bez wprowadzania jakichkolwiek zmian w
interfejsie,
•
dostęp do danych i funkcji składowych obiektu uzyskuje się przez uży-
cie operatora dostępu ’.’ (kropki).
Najważniejsze
sekcje w obrębie klasy to:
•
public – definiuje pola i metody publiczne, które są dostępne z dowol-
nego miejsca programu oraz z modułu, w którym zostały zdefiniowane,
•
private – definiuje pola i metody prywatne, które są niedostępne spoza
modułu (pliku kodu źródłowego), w którym zdefiniowano klasę,
•
protected – definiuje pola i metody chronione, które są widoczne tylko
w danej klasie i w klasach pochodnych (tych, które z niej dziedziczą).
9.2.
Istota programowania obiektowego
Programowanie obiektowe to programowanie, w którym klasy stanowią
najważniejszą część w konstrukcji programu. Podstawowe cechy programowa-
nia obiektowego to:
•
dziedziczenie,
•
hermetyzacja,
•
polimorfizm,
•
rozszerzalność.
Dziedziczenie to budowa jednej klasy na bazie drugiej, przez dodawa-
nie/przesłanianie składowych klasy bazowej. Dziedziczenie umożliwia klasie
pochodnej uzyskanie dostępu do pól i metod swoich klas rodzicielskich. Klasy
potomne dziedziczą charakterystyki i działania klasy rodzic. Klasy pochodne
mogą zmieniać operacje odziedziczone i mogą definiować nowe.
Hermetyzacja (inkapsulacja) to cecha, która pozwala na łączenie danych z
metodami w obrębie obiektu.
Polimorfizm to możliwość dzielenia pojedynczego działania i nazwy dzia-
łania poprzez hierarchię obiektową w sposób właściwy dla każdego obiektu w
tej hierarchii. Polimorfizm pozwala każdej klasie na posiadanie własnych po-
trzeb. Metody te zapewniają jednolitą odpowiedź na komunikaty dochodzące do
żą
danych klas w hierarchii. Polimorfizm umożliwia obiektom generowanie wła-
snych odpowiedzi na podobne komunikaty.
Rozszerzalność pozwala rozszerzać i uaktualniać skompilowane moduły.
125
Przykład 1. Deklaracja klasy z różnymi sekcjami.
Program figura_10;
type
TFigura = class
private
bok : Byte;
podstawa : 1..100;
procedure wypisz;
protected
kat : Integer;
ile_pr : 0..2;
function pole(a,b : Byte) : Integer;
public
nazwa : string;
procedure init;
procedure free;
end;
Przykład 2. Ilustracja definicji metod klasy
.
program figura_02;
type
TProstokat = class
a, b : Integer; // wymiary prostok
ą
ta
function obwod : Integer;
function pole : Integer;
end;
function TProstokat.obwod : Integer;
begin
Result := (a+b)*2;
end;
function TProstokat.pole : Integer;
begin
Result := a*b;
end;
var Pro1, Pro2 : TProstokat;
BEGIN
Pro1 := Tprostokat.Create;
Pro1.a := 4;
Pro1.b := 2;
126
// Pro2 := Tprostokat.Create;
// pro2.a := 10; pro2.b := 20; // bł
ą
d! brak Create!
writeln('obwod= ', Pro1.obwod);
writeln('Pole = ', Pro1.pole);
//writeln('Pole2= ', Pro2.pole);
Pro1.Free;
readln;
END.
Przykład 3. Użycie dwóch klas w jednym programie.
program figury_03;
type
TProstokat = class
a, b : Integer; // wymiary prostok
ą
ta
function obwod : Integer;
function pole : Integer;
end;
function TProstokat.obwod : Integer;
begin
Result := (a+b)*2;
end;
function TProstokat.pole : Integer;
begin
Result := a*b;
end;
type
Trojkat = class
a, b, c : Integer; // wymiary trójk
ą
ta
function obwod : Integer;
function pole : Integer;
end;
function Trojkat.obwod : Integer;
begin
Result := a+b+c;
end;
function Trojkat.pole : Integer;
begin
Result := a*(b+c) div 2;
end;
127
var prost1 : TProstokat; // zmienne obiektowe
trojk1 : Trojkat;
BEGIN
prost1 := TProstokat.Create;
// obowi
ą
zkowe wywołanie konstruktora
prost1.a := 5;
prost1.b := 7;
writeln('obwod pr= ', prost1.obwod);
writeln('Pole pr = ', prost1.pole);
readln;
trojk1 := Trojkat.Create;
trojk1.a := 2;
trojk1.b := 3;
trojk1.c := 4;
writeln('obwod tr= ', trojk1.obwod);
writeln('Pole tr= ', trojk1.pole);
prost1.Free; // obowi
ą
zkowe
trojk1.Free; // obowi
ą
zkowe
readln;
END.
Przykład 4. Ilustracja różnych sposobów dostępu do pól klasy.
program PrKonstr4;
type
TProstokat = class
a : Integer;
procedure ustaw(_d : Integer);
function wypisz : Integer;
private
d : Integer;
protected
b : Integer;
public
c : Integer;
end;
procedure TProstokat.ustaw(_d : Integer);
begin
d := _d;
end;
function TProstokat.wypisz : Integer;
begin
128
Result := d;
end;
var Pro1 : TProstokat;
BEGIN // PROGRAM GŁÓWNY
Pro1 := Tprostokat.Create;
Pro1.a := 4;
Pro1.b := 2;// w module jest dost
ę
p do pola protected
Pro1.c := 50;
// Pro1.d := 1; // brak dost
ę
pu do pola prywatnego
writeln('a = ', Pro1.a);
writeln('b = ', Pro1.b);
writeln('c = ', Pro1.c);
Pro1.ustaw(100);
writeln('d = ', Pro1.Wypisz);
readln;
Pro1.Free
END.
9.3.
Konstruktor i destruktor
•
konstruktor
Create tworzy obiekt (przydziela wymaganą pamięć),
•
destruktor
Free zwalnia przydzieloną pamięć,
•
klasa może zawierać więcej niż jeden konstruktor, a powinna zawierać
jeden destruktor,
•
konstruktor powinien być wywołany jako pierwsza metoda dla nowego
obiektu.
•
wywołanie destruktora kończy pracę z obiektem; aby ponownie korzy-
stać z obiektu, należy ponownie wywołać konstruktor,
•
użycie konstruktora
Create jest obowiązkowe dla każdego egzemplarza
klasy (przydziela wymaganą pamięć), np.
TABC =
class … end;
…
var ob1 : TABC;
ob1 := TABC.Create;
•
użycie destruktor
Free jest obowiązkowe dla każdego egzemplarza kla-
sy (zwalnia przydzieloną pamięć), np.
ob1.Free;
Przykład 1. Użycie konstruktora i destruktora.
program Pr_Konstructor1;
type TProst = class
x, y : Integer;
a, b : Integer; {boki}
129
constructor Create;
destructor Free;
function pole : Longint;
end;
constructor TProst.Create;
begin // Inherited Create;
a := 20; b := 4;
end;
destructor Tprost.Free;
begin // Inherited Free;{ koniec pracy z TKwadrat }
end;
function TProst.pole : Longint;
begin
Result := a*b;
end;
var Pkat : TProst;
BEGIN
Pkat := Tprost.Create;
writeln('Pole prostokata: ', Pkat.pole);
Pkat.a := 100;
writeln('Pole prostokata: ', Pkat.pole);
Pkat.Free;
readln;
END.
Przykład 2. Użycie destruktora i konstruktora.
program pr_konstruktor2;
type TProst = class
a, b : Integer; {boki prostok
ą
ta}
constructor Create(_a,_b : Integer);
destructor Destroy;
function pole : Longint;
end;
constructor TProst.Create(_a, _b : Integer);
begin
Inherited Create; // nieobowi
ą
zkowe
a := _a;
b := _b;
writeln('Działa konstruktor TProst');
130
end;
destructor TProst.Destroy;
begin
writeln('Destruktor - Koniec pracy z TProst');
Inherited Destroy;
end;
function TProst.pole : Longint;
begin
pole := a*b;
end;
var p1, p2 : TProst;
BEGIN
p1 := Tprost.Create(20,30);
p2 := Tprost.Create(4,7);
writeln('Pole prostokata : ', P1.pole);
p1.Destroy; // Pkat.Free;
p2.Destroy;
readln;
END.
9.4.
Typy konstruktorów
Jeżeli nie zostanie zdefiniowany przez użytkownika w klasie żaden kon-
struktor, wówczas kompilator wykorzysta konstruktor klasy TObject, która jest
nadrzędna dla wszystkich klas.
Wyróżniamy trzy rodzaje konstruktorów:
•
domyślny (bez parametrów),
•
zwykły (z parametrami)
−
może być wiele takich konstruktorów,
•
kopiujący.
Jeśli zadeklarujemy jakikolwiek konstruktor pobierający parametry, wtedy
musimy także zadeklarować konstruktor domyślny (bezparametrowy). Przy de-
finicji konstruktora domyślnego wskazane jest inicjalizowanie danych.
Przykład. 1. Program pokazuje użycie kilku konstruktorów w jednej klasie.
Program konstruktory;
type
TPot = class
private
131
x, y : Integer;
public
constructor Create; overload; //domy
ś
lny
constructor Create(dx, dy : Integer); overload;
//zwykły
constructor Create(const dxy : TPot); overload;
//kopiuj
ą
cy
end;
Przykład 2. Użycie konstruktora zwykłego i domyślnego.
program Pr_Konstruktor3;
uses SysUtils;
type
TFig = class
bok : ^Byte; //
↓
konstruktor zwykły
constructor Create(x : Byte); overload;
constructor Create; overload;
destructor Free; //
↑
konstruktor domy
ś
lny
procedure wypisz;
function pole : Integer;
end;
constructor TFig.Create;
begin
New(bok);
end;
constructor TFig.Create(x : Byte);
begin
Create;
bok^ := x;
end;
destructor TFig.Free;
begin
Dispose(bok);
end;
procedure TFig.wypisz;
begin
writeln('bok = ', bok^);
end;
function Tfig.pole : Integer;
132
begin
Result := bok^ * bok^;
end;
var kwadrat : TFig;
BEGIN
kwadrat := TFig.Create(5);
kwadrat.wypisz;
kriteln('Pole kwadratu=', kwadrat.pole)
kwadrat.Destroy;
END.
Przykład 3. Użycie konstruktora domyślnego, zwykłego i kopiującego.
Program Pr_Konstruktor4;
type
TPot = class
private
x, y : Integer;
public
constructor Create; overload; //
←
domy
ś
lny //
↓↓↓↓
zwykły
constructor Create(dx, dy : Integer); overload;
constructor Create(const DK : TPot); overload;
end; //
↑
kopiuj
ą
cy
constructor TPot.Create;
begin
inherited Create;
x := 1; y := 0;
end;
constructor TPot.Create(dx, dy : Integer);
begin
inherited Create;
x := dx;
y := dy;
end;
constructor TPot.Create(const DK : TPot);
begin
inherited Create;
x := DK.x;
y := DK.y;
end;
133
var a, b, c, d : TPot;
BEGIN
a := TPot.Create;
b := TPot.Create( 7,13 );
c := TPot.Create( b );
// tworzenie obiektu przez kopiowanie obiektu b
// d := TPot.Create;
d := b; // d jest tylko wska
ź
nikiem na obiekt b
writeln(' a: ',a.x,' ',a.y);
writeln(' b: ',b.x,' ',b.y);
writeln(' c ',c.x,' ',c.y);
writeln(' d ',d.x,' ',d.y);
a.Free;
b.Free;
c.Free;
// d.Free; // d pokazuje na to samo, co b (d=b)
readln
END.
9.5.
Dziedziczenie
W dziedziczeniu nowy typ klasy tworzony jest na bazie istniejącego typu.
Nowo zdefiniowany typ klasy dziedziczy wszystkie pola i metody bez koniecz-
ności ich definiowania. Typ, od którego dziedziczona jest struktura, nazywa się
typem rodzicem (ang. ancestor type) lub typem bazowym. Typ tworzony od ty-
pu bazowego to typ potomny (ang. descendant type) lub typ pochodny. Dziedzi-
czenie może być pośrednie albo bezpośrednie.
Dziedziczenie metod może być dwojakiego rodzaju:
•
bez przesłaniania
−
deklaracje metod w typie przodka i potomka zawie-
rają inne identyfikatory,
•
z przesłanianiem metod
−
deklaracja metody w typie potomnym ma taki
sam identyfikator, jak metoda w typie bazowym. Wówczas metoda w
typie potomnym przesłania (redefiniuje) metodę odziedziczoną od typu
przodka.
Pola klasy nie mogą być przesłaniane, tzn. żaden typ potomny nie może
posiadać pól o identyfikatorach występujących w typie bazowym. Przesłonięcie
metody statycznej dopuszcza zmiany nagłówka w dowolny sposób. Nowa meto-
da może mieć zarówno inne parametry jak i różną treść.
Procedura nadająca wartości polom w typie pochodnym może wykorzysty-
wać procedurę nadającą wartości polom typu bazowego i określać tylko dodat-
kowe pola typu pochodnego.
134
Przykład. Aby program działał prawidłowo, należy klasę potomka uzupełnić
o metodę wykonaj, o identycznej treści jak w klasie TRodzic.
Program dziedicz_1;
type TRodzic = class
procedure napisz;
end;
procedure TRodzic.napisz;
begin
writeln('to napisał Rodzic');
end;
type TPotomek = class(TRodzic)
procedure napisz;
end;
procedure TPotomek.napisz;
begin
Inherited napisz;
writeln('To dopisało
ż
ycie (Potomek)');
//writeln('A to dopisało
ż
ycie (Potomek)');
end;
var potomek : TPotomek;
BEGIN
Potomek := TPotomek.Create;
Potomek.napisz;
readln;
Potomek.Free;
END.
9.6.
Polimorfizm
Polimorfizm (wielopostaciowość) jest mechanizmem, dzięki któremu ta
sama metoda (o tej samej nazwie (i parametrach)) może mieć różne skutki dzia-
łania (różne znaczenie) w zależności od tego, jaki obiekt ją wywołuje.
Dziedziczenie homomorficzne zachodzi wtedy, gdy połączenia miedzy me-
todami obiektów są wykonywane w trakcie kompilacji programu.
Dziedziczenie polimorficzne polega na korzystaniu z tablic metod wirtual-
nych (VMT) dla każdej klasy, w której są adresy metod w danej klasie.
Wzajemne wywoływanie metod odbywa się za pośrednictwem adresów zawar-
135
tych w tablicach VMT, a te są różne dla klasy przodka i potomka. Metoda w kla-
sie przodka prawidłowo rozpozna wywoływaną metodę również w klasie po-
tomka.
W dziedziczeniu polimorficznym należy używać metod wirtualnych,
tj. w nagłówku metody przodka trzeba wpisać dyrektywę
virtual, np. procedu-
re metoda; virtual; ale w deklaracji nagłówka metody potomka należy użyć dy-
rektywy
override, np. procedure metoda; override;
Przykłady dziedziczenia:
Dziedziczenie homomorficzne
Dziedziczenie polimorficzne
program dziedzicz_homomorf;
type TRodzic = class
procedure napisz;
procedure wykonaj;
end;
procedure TRodzic.napisz;
begin
writeln('To napisał >> Rodzic');
end;
procedure TRodzic.wykonaj;
begin
napisz;
end;
type TPotomek = class(TRodzic)
procedure napisz;
end;
procedure TPotomek.napisz;
begin
writeln('to napisał > Potomek' );
end;
var potomek :TPotomek;
BEGIN
Potomek:= TPotomek.Create;
Potomek.wykonaj;
Readln;
Potomek.Free;
END.
program dziedzicz_polimorf;
type TRodzic = class
procedure napisz; virtual;
procedure wykonaj;
end;
procedure TRodzic.napisz;
begin
writeln('To napisał >> Rodzic');
end;
procedure TRodzic.wykonaj;
begin
napisz;
end;
type TPotomek = class(TRodzic)
procedure napisz; override;
end;
procedure TPotomek.napisz;
begin
writeln('to napisał > Potomek' );
end;
var potomek :TPotomek;
BEGIN
Potomek:= TPotomek.Create;
Potomek.wykonaj;
Readln;
Potomek.Free;
END.
136
9.7.
Metody wirtualne
W Lazarus FPC metody wirtualne mogą być również pokrywane metodami
statycznymi albo innymi metodami wirtualnymi. Z tego powodu, przy dziedzi-
czeniu polimorficznym, konieczne jest używanie dyrektywy
override w klasie
potomka. Jeżeli metodę wirtualną zakrywamy metodą statyczną (brak
override),
rezygnujemy z polimorfizmu i kompilator wyświetla ostrzeżenie, dlatego dyrek-
tywę
override należy zastąpić dyrektywą reintroduce.
W dziedziczeniu polimorficznym należy używać metod wirtualnych.
W nagłówku metody przodka należy użyć dyrektywy
virtual, np.:
procedure metoda; virtual;
W deklaracji nagłówka metody potomka należy jednak użyć
override, np.:
procedure metoda; override;
Przykład 1. Ilustracja użycia dyrektywy reintroduce.
program dziedzicz_reintro;
type TRodzic = class
procedure dodaj(x : Byte); virtual;
end;
procedure TRodzic.dodaj(x : Byte);
begin
writeln(x+10);
end;
type TPotomek = class(TRodzic)
procedure dodaj(x : Byte); reintroduce;
end;
procedure TPotomek.dodaj(x : Byte);
begin
writeln(x, ' + dziesiec');
end;
var potomek : TPotomek;
BEGIN
Potomek := TPotomek.Create;
Potomek.dodaj(1);
readln;
Potomek.Free;
END.
137
Przykład 2. Dziedziczenie na zasadzie rodzic
→
potomek
→
wnuk.
program dziedziczenie_wnuk;
type TRodzic = class
procedure dodaj(x : Byte); virtual;
procedure wykonaj(b : Byte);
end;
procedure TRodzic.dodaj(x : Byte);
begin
writeln(x+10);
end;
procedure TRodzic.wykonaj(b : Byte);
begin
dodaj(b);
end;
type TPotomek = class(TRodzic)
procedure dodaj(x : Byte); override;
end;
procedure TPotomek.dodaj(x : Byte);
begin
writeln(x,' + sto');
end;
type TWnuk = class(TPotomek)
procedure dodaj(x : Byte); override;
end;
procedure TWnuk.dodaj(x : Byte);
begin
writeln(x,' plus 1000');
end;
var Pot : TRodzic;
BEGIN
Pot := TRodzic.Create;
Pot.wykonaj(1);
Pot.Free;
Pot := TPotomek.Create;
Pot.wykonaj(1);
Pot.Free;
138
Pot := TWnuk.Create;
Pot.wykonaj(1);
Pot.Free;
readln;
END.
9.8.
Metody dynamiczne
Dziedziczenie polimorficzne zapewniają:
•
metody wirtualne deklarowane słowem
virtual,
•
metody dynamiczne deklarowane słowem
dynamic.
Metody te działają tak samo, ale różnią się sposobem optymalizacji. Meto-
da dynamiczna daje mniejszy rozmiar kodu wynikowego programu po kompila-
cji, natomiast metoda wirtualna powoduje, że program po kompilacji działa
szybciej niż dla metody dynamicznej. Z polimorfizmem mamy do czynienia
również w przypadku wywołania metod z parametrem aktualnym typu potom-
nego względem typu parametru formalnego.
Przykład. Dziedziczenie na zasadzie Fig(Figura)
→
Pr(Prostokąt).
program dziedzicz_dyn;
type TFig = class
x, y : Integer;
nazwa: string[20];
constructor Create;
destructor Destroy;
procedure narysuj;
procedure zmaz;
end;
constructor TFig.Create;
begin
inherited Create;
x := 1; y := 2;
end;
destructor TFig.Destroy;
begin zmaz;
inherited Destroy;
end;
139
procedure TFig.narysuj;
begin
writeln('Narysowano figur
ę
x=',x,' y=',y);
end;
procedure TFig.zmaz;
begin
writeln('Zmazano figure x=',x,' y=',y,' ',nazwa);
end;
// klasy dziedzicz
ą
ce z TFig
type TProst = class(TFig)
a, b : Integer;
constructor Create(a_, b_: Integer; name : string);
procedure narysuj;
procedure zmaz_np;
procedure przesun(x1,y1 : Integer);
end;
constructor TProst.Create(a_,b_:Integer; name :string);
begin
inherited Create;
a := a_; b := b_;
nazwa:=name;
end;
procedure TProst.narysuj;
begin
write('+ Rysowanie prostok
ą
ta x=',x,' y=',y);
writeln('+ o rozmiarach a=',a,' b=',b);
end;
procedure TProst.zmaz_np;
begin
writeln('Zmazano prostok
ą
t x=',x, 'y=',y, ' ',nazwa);
end;
procedure TProst.przesun(x1,y1 : Integer);
begin
zmaz; // zmaz_np;
x := x1; y := y1;
narysuj;
end;
140
var Fig : TFig;
Pr1 : TProst;
BEGIN
Fig := TFig.Create;
Fig.narysuj;
Fig.Destroy; // Fig.Free;
Fig := TProst.Create(2,2,'Kwadrat');
Fig.narysuj; // Fig.przesun(10, 10);
Fig.Destroy;
Pr1 := Tprost.Create(10,15,'Prost 1');
Pr1.narysuj;
Pr1.przesun(100, 100);
Pr1.zmaz_np;
Pr1.zmaz;
Pr1.Free;
readln;
END.
141
10.
Programowanie obiektowe - aplikacje
LCL jest biblioteką wizualnych komponentów, stworzoną w języku Object
Pascal, na potrzeby środowiska Lazarus
. Biblioteka LCL należy do jednych z
bardziej przejrzystych i dobrze zaprojektowanych bibliotek wspomagających
programowanie w środowisku Lazarus, zwłaszcza tworzenie interfejsu użyt-
kownika. Podstawą biblioteki LCL jest klasa bazowa TClass.
VCL jest biblioteką wizualnych komponentów, stworzoną w języku Object
Pascal przez firmę Borland, na potrzeby środowiska Delphi, a później zaadap-
towaną również do środowiska C++ Builder. Obecnie biblioteka VCL integruje
w sobie dodatkowo możliwość korzystania z technologii .NET firmy Microsoft.
Podstawą biblioteki VCL jest klasa bazowa TObject. Z niej dziedziczą wszystkie
pozostałe klasy biblioteki.
VCL to skrót od ang. Visual Component Library (Biblioteka Wizualnych
Komponentów). Jest jednym z najważniejszych elementów Delphi. To właśnie
niej zawdzięczamy łatwość i szybkość projektowania aplikacji. Biblioteka VCL
składa się z gotowych, napisanych wcześniej, komponentów, które podczas
kompilacji są dołączane do wykonywalnego pliku naszego programu. Po skom-
pilowaniu program nie wymaga żadnych dodatkowych plików. Może być uru-
chamiany na dowolnym komputerze, bez względu na rodzaj zastosowanych
komponentów.
Standardowe komponenty VCL/LCL nadają się do większości zastosowań.
Gdybyśmy jednak potrzebowali jakichś specjalnych funkcji, możemy doinsta-
lować dodatkowe komponenty (w sieci jest ich bardzo wiele) lub napisać wła-
sny. Zbiór komponentów to zbiór elementów, z których poprzez odpowiednich
ich dobór, można w krótkim czasie zbudować aplikację. Komponenty posiadają
określone metody, właściwości oraz zdarzenia. Te pojęcia określają sposób
komunikowania się komponentu z otoczeniem.
10.1.
Komponenty LCL
Ś
rodowisko Lazarus zawiera zbiór podstawowych komponentów wykorzy-
stywanych do tworzenia aplikacji. Są to przyciski, okienka, napisy, suwaki, pola
wyboru, menu, tabelki itp. Komponenty zgrupowane są w kilku zakładkach.
Istnieje możliwość dodania nowych komponentów i stworzenia własnych. Kom-
ponenty LCL znajdują się na pasku, pod listwą Menu (rys. 10.1).
142
Rys. 10.1. Widok paska menu środowiska Lazarus
Grupa Standard zawiera najczęściej wykorzystywane komponenty. Służą one
do projektowania wyglądu aplikacji. Oto jej składniki:
TFrames - Ramki (frames) mają podobne
właściwości jak formularze (forms), z tym
wyjątkiem, że ramka może być osadzona
wewnątrz formy. Wprowadzenie ramek
bardzo ułatwiło projektowanie wyglądu
niektórych aplikacji.
TMainMenu - główne menu danego for-
mularza.
TPopUpMenu - menu wyświetlane po
kliknięciu prawym przyciskiem myszki na
danym obiekcie.
TLabel – (etykieta) pole służące do wy-
ś
wietlania tekstu.
TEdit - pole służące do edycji jednego
wiersza tekstu.
TMemo - pole tekstowe z możliwością
edycji, odczytu i zapisu wyświetlanego
tekstu.
TButton – przycisk.
TCheckBox - pole wyboru.
TRadioButton - pole wyboru jednej z kil-
ku opcji.
TListBox - wyświetla listę elementów.
TComboBox - wyświetla listę elementów
z możliwością wpisania tekstu.
TScrollBar - pasek przewijania.
Rys. 10.2. Widok okna komponentów
– zakładka Standard
143
TGroupBox - grupuje kilka komponentów np. typu RadioButton lub CheckBox.
TRadioGroup - grupuje komponenty RadioButton.
TPanel - komponent ogólny w kształcie prostokąta, w którym można umiesz-
czać inne komponenty.
ActionList - komponent pozwalający na dodawanie własnych procedur obsługi
do niektórych akcji wykonywanych przez użytkownika.
Grupa Additional zawiera uzupełniające kom-
ponenty, kształtujące wygląd naszej aplikacji
oraz ułatwiające komunikację z użytkownikiem.
TBitBtn – przycisk, na którym umieszczony jest
rysunek (Ikona).
TSpeedButton - przycisk umieszczany w pasku
narzędzi.
TMaskEdit - pole edycji pozwalające na filtro-
wanie i formatowanie danych wprowadzanych
przez użytkownika.
TStringGrid – arkusz, którego elementami są
łańcuchy znaków.
TDrawGrid - arkusz przeznaczony do wprowa-
dzania danych innych niż tekstowe.
TImage - komponent wyświetlający grafikę.
TShape - figura geometryczna.
TBevel - tworzy wypukłe lub wklęsłe linie, pro-
stokąty, lub ramki.
TScrollBox - przewijane okienko mogące za-
wierać inne komponenty.
TCheckListBox - przewijana lista z możliwością
zaznaczenia poszczególnych pozycji.
TSplitter - służy do przesuwania części okienka.
TStaticText – tekst statyczny, komponent dzia-
łający podobnie jak Label.
TControlBar - pasek narzędzi z możliwością
przestawiania poszczególnych pozycji.
ApplicationEvents - niewizualny komponent
umożliwiający obsługę globalnych zdarzeń
aplikacji.
TLabeledEdit - pole edycyjne z tekstem opisu.
TColorBox - okno kolorów systemowych.
Rys. 10.3. Widok okna komponentów
– zakładka Additional
144
Grupa System
Na tej karcie znajdują się komponenty korzystające bezpośrednio z funkcji
systemowych, np. TTimer wywołuje zadaną procedurę w określonych odstępach
czasu.
Grupa Dialogs zawiera komponenty wywo-
łujące różnego typu okienka dialogowe:
TOpenDialog - okienko otwierania pliku.
TSaveDialog - okienko zapisywania pliku.
TOpenPictureDialog - okienko otwierania
pliku z podglądem graficznym.
TSavePictureDialog - okienko zapisywania
pliku z podglądem graficznym.
TFontDialog - okienko wyboru czcionki.
TColorDialog - okienko wyboru koloru.
TPrintDialog - okienko drukowania.
TPrinterSetupDialog - okienko ustawień dru-
karki.
TFindDialog - okienko obsługujące procedu-
ry przeszukiwania.
TReplaceDialog - okienko obsługujące pro-
cedury zamiany zadanej frazy.
TPageSetupDialog - okienko ustawień strony.
Rys. 10.4. Widok okna komponentów
– zakładka Dialogs
10.2.
Inspektor obiektów
Inspektor obiektów pozwala zobaczyć wszystkie komponenty użyte w
programie. Pokazuje jednocześnie jak są one rozmieszczone na formie. Ułatwia
szybkie przełączanie się między nimi. Każdy obiekt widoczny w
Inspektorze
obiektów ma wyświetloną nazwę i klasę źródłową. Znajdują się w nim również
cztery sekcje:
Właściwości, Zdarzenia, Ulubione, Ograniczenia.
Sekcja
Właściwości zawiera właściwości używanych obiektów. Wiele war-
tości jest ustalanych domyślnie w trakcie inicjalizacji komponentu. Wartości
domyślne można zmieniać w trakcie projektowania aplikacji lub w kodzie.
145
W sekcji
Zdarzenia można generować podprogramy, które reagują na okre-
ś
lone zdarzenia związane z danym komponentem. Zakładka ta składa się z
dwóch kolumn: lewa zawiera nazwy poszczególnych zdarzeń, a prawa
−
proce-
dury przypisane do nich. Na przykład, zdarzenie OnClick odpowiada sytuacji,
gdy dany komponent zostanie kliknięty myszką, a przypisany podprogram wy-
kona określoną operację.
10.3.
Okno komunikatów i okno Form
Komunikaty to opisy, uwagi, ostrzeżenia i błędy generowane przez kompi-
lator Lazarus. Pokazywane są w oknie. Komunikaty stanowią podstawowe źró-
dło informacji o problemach uniemożliwiających uruchomienie programu.
Rys. 10.5. Widok okna komunikatów
Okno Form:
•
okno graficzne, na którym rozmieszcza się komponenty aplikacji,
•
obsługuje tryb WYSIWYG (ang. What You See Is What You Get),
•
umożliwia zmianę właściwości danego komponentu oraz wygenerowa-
nie kodu obsługi zdarzenia OnClick poprzez kliknięcie na nim.
10.4.
Edytor źródeł
Edytor źródeł umożliwia podgląd oraz edycję kodu źródłowego tworzone-
go programu. Ważne fragmenty kodu są wyróżnione kolorem lub tłustym dru-
kiem. Domyślnie są uwzględniane wcięcia w kodzie.
Pliki tworzone przez edytor źródeł to:
•
Plik główny projektu z rozszerzeniem .LPR
−
plik tekstowy, który za-
wiera informacje o formularzach i modułach aplikacji. Znajduje się tam
również kod, który inicjalizuje aplikację.
•
Plik projektu z rozszerzeniem .LPI
−
plik tekstowy, który zawiera in-
formacje o konfiguracji środowiska Lazarus w formacie xml.
146
•
Plik modułu z rozszerzeniem .PAS
−
plik tekstowy zawierający kod
ź
ródłowy modułu w języku Object Pascal. Może być stowarzyszony z
formularzem lub stanowić samodzielny składnik projektu.
•
Plik formularza z rozszerzeniem .LFM
−
plik binarny zawierający defi-
nicję formularza. Każdy taki plik powiązany jest z modułem (plik
.PAS), zawierającym program źródłowy związany z obsługą formularza.
•
Plik zasobów z rozszerzeniem .RES
−
binarny plik zasobów projektu.
Rys. 10.6. Widok okna Edytora źródeł
W efekcie kompilacji tworzony jest wykonywalny plik wynikowy (pro-
gram) z rozszerzeniem .EXE. Jest to plik wygenerowany przez projekt. Tworzo-
ne są także pliki modułów z rozszerzeniem .PPU. Zawierają one skompilowane
wersje modułów zawartych w projekcie.
Każdy program składa się z wielu plików, które mają takie same nazwy, ale
różne rozszerzenia.
Wysoce zalecane jest przechowywanie (zapisywanie) pli-
ków każdego projektu w oddzielnym folderze.
147
10.5.
Obiekt Button1 klasy TButton
Rys. 10.7. Widok komponentu Button1
Umieszczenie na formie komponentu Button z palety Standard powoduje
pojawienie się na formie prostokątnego przycisku z napisem Button1.
Zaznaczenie go pojedynczym kliknięciem pozwala w okienku Inspektora
obiektów zapoznać się z jego właściwościami oraz zdarzeniami.
Rys. 10.8. Widok okna Inspektora obiektów z komponentem Button1 klasy TButton
148
•
W oknie
Inspektora obiektów są widoczne właściwości komponentu
Button1. Zmiana właściwości powoduje m.in. zmianę jego położenia,
rozmiaru, koloru, nazwy i wielu innych parametrów.
•
Właściwości przycisku można zmieniać bezpośrednio z poziomu
In-
spektora obiektów (w czasie projektowania aplikacji) albo programo-
wo, za pomocą odpowiednich instrukcji w kodzie źródłowym aplikacji.
Przykładowe polecenie zmieniające właściwość Caption komponentu
Button1: Button1.Caption := 'Nowy tekst nagłówka';
Zmiana dowolnej właściwości jest natychmiast aktualizowana. W pewnych
przypadkach niektóre właściwości są tylko do odczytu (ang. read only), znaczy
to że nie możemy danej właściwości zmienić
−
możemy ją jednak odczytać.
Pełną listę właściwości znajdziemy w systemie pomocy. Po zaznaczeniu
komponentu i naciśnięciu klawisza F1 wyświetli się okienko z opisem dostęp-
nych metod, właściwości i zdarzeń.
W oknie
Inspektora obiektów znajduje się sekcja Zdarzenia (ang.
Events). Każdy komponent posiada swój zbiór zdarzeń, które możemy oprogra-
mować. Nazwa każdego zdarzenia składa się z przedrostka On i tekstu informu-
jącego, czego dotyczy dane zdarzenie.
Zdarzenia mają za zadanie sprawdzać, czy nie wystąpiła określona czyn-
ność i w przypadku jej wystąpienia wywołać odpowiadającą jej procedurę. Kli-
kając dwukrotnie na pole obok nazwy wybranego zdarzenia przechodzimy do
edycji kodu procedury jego obsługi.
Najczęściej używanym zdarzeniem jest zdarzenie OnClick reagujące na
kliknięcie myszką na danym komponencie.
Inny dostęp do zdarzenia uzyskuje się po dwukrotnym kliknięciu na kom-
ponent. Oprócz właściwości i zdarzeń, komponenty posiadają również metody.
Są one widoczne w
Inspektorze obiektów, w zakładce Właściwości. Metody są
to funkcje i procedury, które wykonuję na komponencie określone operacje.
Przykładowo, metodę powodującą że przycisk staje się niewidoczny wywołuje-
my następującym poleceniem: Button1.Hide;
Tabela 10.1. Lista wybranych właściwości klasy TControl
widocznych w oknie Inspektora obiektów
Nazwa
Opis
Align
Określa domyślne położenie komponentu
AutoSize
Automatyczne utrzymywanie pierwotnego rozmiaru komponentu
149
Caption
Napis na komponencie (tekst)
Height
Wysokość komponentu
Width
Szerokość komponentu
Color
Kolor tła obiektu
Cursor
Kursor wyświetlany po umieszczeniu wskaźnika myszy nad
obiektem
Enabled
Określa, czy obiekt jest aktywny czy też nie
Font
Czcionka używana przez komponent
Hint
Wskazówka (etykietka podpowiedzi), pokazywana po umiesz-
czeniu kursora nad obiektem
Name
Nazwa komponentu
Visible
Właściwość określająca, czy komponent ma być widoczny pod-
czas działania programu
Tabela 10.2. Lista wybranych zdarzeń TControl
widoczna w oknie Inspektora obiektów
Nazwa
Opis
OnClick
Aplikacja podejmuje działanie po kliknięciu myszką na danym
komponencie
OnClose
Zdarzenie związane z zamykaniem okna
OnDragDrop
Zdarzenie generowane jest w momencie, gdy w komponencie na-
stępuje „upuszczenie” danych przeciąganych metodą „drag and
drop”
OnKeyDown
Akcja po naciśnięciu dowolnego klawisza
OnKeyPress
Akcja po przytrzymaniu dowolnego klawisza
OnMouseDown
Akcja po kliknięciu w obszarze komponentu
OnMouseMove
Akcja po ruchu kursora myszki na komponentem
OnEnter
Akcja na wejście do komponentu
OnExit
Akcja po wyjściu z komponentu
150
10.6.
Aplikacja Stoper
Zbudujemy stoper elektroniczny na bazie komponentu TTimer:
•
Stoper1: w tej wersji wykorzystane zostaną komponenty TTimer oraz
TLabel. Stoper będzie zliczał sekundy i minuty od chwili 00:00.
•
Stoper2: zmodyfikujemy Stoper1 tak, aby zliczanie sekund następowało
co 200ms, oraz zbudujemy funkcję wyświetlania czasu, która minuty i
sekundy zawsze wyświetli dwucyfrowo.
•
Stoper3: wykorzystamy komponenty TTimer, TLabel oraz TButton
(x3); przycisk
Start (Button1) uruchomi stoper, przycisk Stop (Button2)
zatrzyma stoper, przycisk
Koniec (Button3) zamknie aplikację.
•
Stoper4: wykorzystamy komponenty TPanel (x2), TTimer, TButton
(x4) i TLabel; przycisk
Reset uruchomi stoper od nowa, przycisk Stop
zatrzyma stoper,
Start ponowi zliczanie sekund, przycisk Koniec za-
mknie aplikację. Cyfry stopera umieścimy w TLabel, na Panel1 ułoży-
my wszystkie przyciski, na Panel2 umieścimy stoper.
Aplikacja Stoper1:
1. Otworzyć aplikację VCL.
2. Wstawić komponenty typu TLabel i TTimer.
3. W polu Caption formularza Form1 wpisać „Stoper1”, zmienić nazwę kompo-
nentu Label1 na Label00_00 (zmiana w polu Name).
4. W polu Caption komponentu Label00_00 wpisać 00:00, w polu Font ustawić
rozmiar czcionki na 48, zmienić kolor (np. na niebieski).
5. Wygenerować procedurę Timer1Timer klikając dwukrotnie na ikonie
komponentu Timer1 (w
Inspektorze obiektów, w zakładce Zdarzenia, zo-
staje włączona metoda i dołączona do kodu klasy Form1).
6. W procedurze
Timer1Timer wpisać poniższy kod zliczania sekund i minut.
// procedura Timer1Timer, która inkrementuje zmienną
sec
// co 1000 ms, czyli co sekundę i zlicza też minuty,
// procedura Timer1Timer pochodzi od obiektu Timer1
procedure TForm1.Timer1Timer(Sender: TObject);
begin
inc(sek); //zliczamy co 1000ms
if sek = 60 then
begin
sek := 0;
inc(min);
151
end;
Label00_00.Caption := IntToStr(min)+':'+IntToStr(sek);
end;
Rys. 10.9. Widok środowiska Lazarus – budowa aplikacji Stoper1
Komponenty graficzne opisane są w pliku tekstowym Unit1.pas.
Kod aplikacji
Stoper1 (formularza Form1) znajduje się w module Unit1.pas:
unit Unit1;
{$mode objfpc}{$H+}
Interface
uses Classes, SysUtils, FileUtil, LResources, Forms,
Controls, Graphics, Dialogs, StdCtrls, ExtCtrls;
type
{ TForm1 }
TForm1 = class(TForm)
Label_00: TLabel;
Timer1: TTimer;
procedure Timer1Timer(Sender: TObject);
private { private declarations }
public { public declarations }
152
min, sek: Integer;
end;
var Form1: TForm1;
implementation
{ TForm1 }
procedure TForm1.Timer1Timer(Sender: TObject);
begin
inc(sek); // tu b
ę
dziemy co 1000ms
if sek >= 60 then
begin
sek := 0;
inc(min);
end;
Label_00.Caption := IntToStr(min)+':'+IntToStr(sek);
end;
initialization
{$I unit1.lrs}
end.
Plik główny aplikacji, czyli stoper1.lpr ma postać:
program project1;
{$mode objfpc}{$H+}
uses
{$IFDEF UNIX}{$IFDEF UseCThreads}
cthreads,
{$ENDIF}{$ENDIF}
Interfaces, // this includes the LCL widgetset
Forms, Unit1, LResources
{$IFDEF WINDOWS}{$R project1.rc}{$ENDIF}
BEGIN
{$I stoper1.lrs}
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
END.
153
Aplikacja Stoper3:
Rys. 10.10. Widok formularza i aplikacji Stoper3
Zmienne
sek
i
min
definiujemy jako składniki klasy Form1.
TForm1 = class(TForm)
…
public { Public declarations }
min, sek : Integer;
Treść procedury dla przycisku
Start:
procedure TForm1.Button1Click(Sender: TObject);
begin
sek:=0; min:=0; //zerowanie zmiennych
Label1.Caption := '00:00'; //wyzerowanie stopera
Timer1.Enabled := True; //wł
ą
czenie stopera
end;
Treść procedury dla przycisku
Stop:
procedure TForm1.Button2Click(Sender: TObject);
begin
Timer1.Enabled := False; // zatrzymanie stopera
end;
Treść procedury dla przycisku
Koniec:
procedure TForm1.Button3Click(Sender: TObject);
begin
Close; // zamkni
ę
cie aplikacji
end;
Treść procedury dla komponentu Timer1:
//zliczanie czasu stopera3
procedure TForm1.Timer1Timer(Sender: TObject);
begin
154
//tu b
ę
dziemy co 1000ms
inc(sek); //zlicza sekundy
if sek >= 60 then
Begin
sek := 0;
inc(min); //zlicza minuty
end;
Label1.Caption := IntToStr(min)+ ':'+IntToStr(sek);
end;
10.7.
Aplikacja Kalkulator
Zbudować czterodziałaniowy kalkulator arytmetyczny na liczbach całkowi-
tych (lub rzeczywistych).
Założenia:
•
dane (argumenty) będą wpisywane w dwóch oknach tekstowych,
•
wynik będzie umieszczany w (trzecim) oknie tekstowym,
•
działania arytmetyczne + – * / będą wywoływane przyciskami TButton,
•
dodatkowe dwa przyciski:
C – wyczyści wynik; Zamknij (lub Koniec)
– zamknie aplikację.
Rys. 10.11. Widok formularza i aplikacji kalkulatora 4-działaniowego
Wszystkim komponentom należy wpisać nazwy odpowiednie do ich funk-
cji. Trzeba tego dokonać w oknie
Inspektora obiektów, w polu Name po klik-
nięciu wybranego obiektu. Przy okazji zmienić napisy na komponentach w po-
lach Caption oraz wielkość czcionki w polach Font (na 20 pkt.).
155
Tabela 10.3. Ustawienia komponentów w aplikacji Kalkulator
Name
Caption
Component
Label1_a
a
Label1
Label2_b
b
Label2
Label3_wyn
Wynik
Label3
B_Dodaj
+
Button1
B_Odejm
−
Button2
B_Mnoz
*
Button3
B_Dziel
/
Button4
B_Czysc
C
Button5
B_Koniec
Koniec
Button6
Panel1
Kalkulator
Panel1
Name
Text
Component
x1
znak pusty
Edit1
x2
znak pusty
Edit2
Wynik
znak pusty
Edit3
Poprzez podwójne kliknięcie lewym przyciskiem myszy wygenerować pro-
cedury dla każdego przycisku. Zmienne dla argumentów zdefiniować jako
zmienne globalne modułu lub pola w klasie Form1.
var a, b, c : Integer; // zmienne modułu unit1.pas
public
a, b, c : Integer; // pola klasy Form1.
procedure TForm1.B_DodajClick(Sender: TObject);
begin //zamiana tekstu w oknach edycji x1 i x2 na liczby
a:=strtoInt(x1.text);
b:=strtoInt(x2.text);
c:=a+b; //dodawanie liczb
wynik.text:=inttostr(c); //zamiana liczby c na tekst
Label3_wyn.caption:='suma';//zmienny komentarz wyniku
end;
procedure TForm1.B_OdejmClick(Sender: TObject);
begin
a:=strtoint(x1.text);
b:=strtoint(x2.text);
c:=a-b;
wynik.text:=inttostr(c);
Label3_wyn.caption:= 'ró
ż
nica';
end;
procedure TForm1.B_MnozClick(Sender: TObject);
begin
a:=strtoint(x1.text);
156
b:=strtoint(x2.text);
c:=a*b;
wynik.text:=inttostr(c);
Label3_wynik.caption:='iloczyn';
end;
procedure TForm1.B_DzielClick(Sender: TObject);
begin
a:=strtoint(x1.text);
b:=strtoint(x2.text);
if b<>0 then c:=a div b else c:=0;
if b<>0 then wynik.text:=inttostr(c)
else begin wynik.Text:='Error!';
showmessage('Nie dziel przez zero !');
end;
//wyswietlenie komunikatu w dodatkowym oknie
Label3_wynik.caption:='iloraz';
end;
procedure TForm1.B_czyscClick(Sender: TObject);
begin
x1.text:=''; //czyszczenie okna edycji x1
x2.text:='';
wynik.text:=''; //czyszczenie okna edycji wyniku
label3_wynik.caption:='wynik';
end;
procedure TForm1.B_KoniecClick(Sender: TObject);
begin
Close; //zamkni
ę
cie aplikacji
end;
10.8.
Aplikacja Równanie kwadratowe
Zadanie polega na obliczaniu i wyświetlaniu pierwiastków równania kwa-
dratowego oraz rysowaniu wykresu odpowiadającej mu paraboli. Założenia:
•
współczynniki a, b i c równania będą podawane w oknach SpinEdit,
•
wyniki (pierwiastki) będą wyświetlane w oknach edycji TEdit,
•
komentarze pojawią się w etykietach TLabel,
•
wykres zostanie sporządzony w komponencie TChart,
•
zaprogramowana zostanie akcja na przycisk.
Rozważone zostaną cztery przypadki:
•
dwa pierwiastki rzeczywiste różne,
•
jeden pierwiastek podwójny – widoczna jedna wartość w jednym oknie,
•
brak pierwiastków – odpowiedni komunikat i brak okien,
•
rozwiązanie równania liniowego, gdy a = 0.
157
Formularz aplikacji i rozmieszczenie komponentów pokazano na rys. 10.12.
Rys. 10.12. Widok formularza aplikacji do rozwiązywania równań kwadratowych
Wstawianie wykresu:
1.
wstaw komponent TChart,
2.
wykonaj szybkie podwójne kliknięcie lewym przyciskiem na wykresie.
Pojawi się okno wstawiania
linii do wykresów (Edit series – Chart1).
Wybierz
Add (wstawianie komponentu ChartSeriesEdit), czyli dodaj
jedną
linię na wykresie.
3.
sformatuj linię wykresu, używając właściwości SeriesColor, Title itp.
Rys. 10.13. Okno do wstawiania/usuwania linii z wykresu
158
Kod modułu unit1, czyli rozwiązanie zadania:
Unit unit1;
TForm1 = class(TForm)
Button1: TButton; //oblicz
Chart1: TChart; // wykres - siatka
wynikx1: TEdit; // okno wyniku pierwszego pierwiastka
wynikx2: TEdit; // okno wyniku drugiego pierwiastka
chart2LS: TLineSeries; //wykres – linia funkcji kw.
funckja: TLabel; // opis
lx1: TLabel; // opis
lx2: TLabel;
lax2: TLabel;
lbx: TLabel;
lc: TLabel;
abox: TFloatSpinEdit; // współczynnik równania (a)
bbox: TFloatSpinEdit; // współczynnik równania (b)
cbox: TFloatSpinEdit; // współczynnik równania (c)
procedure Button1Click(Sender: TObject);
//procedura obliczania pierwiastków równania
private
{ private declarations }
public
{ public declarations }
end;
var Form1: TForm1;
implementation
{$R *.lfm}
{ TForm1 }
procedure TForm1.Button1Click(Sender: TObject);
const
MARGIN = 10; //definiuje margines
var
a: Double; // parametr a
b: Double; // parametr b
c: Double; // parametr c
x1, x2: Double; // x1 i x2 pierwiastki równania kwadr.
delta: Double; // wyró
ż
nik trójmianu kwadratowego
// zmienne potrzebne do p
ę
tli for i wykresu
x, y: Double;
i: Integer;
mlewy, mprawy: Longint; //marginesy
159
begin
a := abox.Value; //dane ze SpeedButtons
b := bbox.Value;
c := cbox.Value;
if a<>0 then //sprawdzamy, czy a jest ró
ż
ne od zera
begin
// piszemy posta
ć
ogoln
ą
funkcji
funckja.caption := FloatToStr(a)+'x^2+'+FloatToStr(b)
+'x+'+FloatToStr(c);
//obliczamy delt
ę
ze wzoru: b^2 - 4*a*c
delta := b*b-(4*a*c);
x1 := 0;
x2 := 0;
if delta>0 then //je
ż
eli delta>0, to 2 pierwiastki
begin
x1 := (-b+sqrt(delta))/(2*a);//obliczenie pierwiastków
x2 := (-b-sqrt(delta))/(2*a);
wynikx1.Text := FloatToStr(x1);
//wy
ś
wietlenie wyników
wynikx2.Text := FloatToStr(x2);
if x2>x1 then //ustalenie marginesu wykresu
begin
mprawy := round(x1)+MARGIN;
mlewy := round(x2)-MARGIN;
end
else
begin
mlewy := round(x1)-MARGIN;
mprawy := round(x2)+MARGIN;
end;
end
else
if delta=0 then //je
ż
eli delta=0, to 1 pierwiastek
begin
x1 := -b/(2*a); // wzor na pierwiastek
wynikx1.Text := FloatToStr(x1);//wy
ś
wietlenie wyn.
wynikx2.Text := FloatToStr(x1);
mlewy := round(x1-MARGIN);
mprawy := round(x1+MARGIN); //ustalenie marginesu
end
else
if delta<0 then // brak pierwiastków rzeczywistych
begin
160
wynikx1.Text := 'brak miejsc zerowych';
//wyswietlenie wyników
wynikx2.Text := '';
mlewy := -5; //ustalenie marginesu wykresu
mprawy := 5;
end;
end
else //funkcja liniowa
begin
x1 := 0; //zerowanie pierwiastków
x2 := 0;
funckja.caption := FloatToStr(b)+'x+'+FloatToStr(c);
//wy
ś
wietlenie funkcji liniowej
if c <> 0 then
// je
ż
eli parametr c<>0, to:
begin
x1 := (-b)/c;
wynikx1.Text := FloatToStr(x1);
//wyswietlenie wyników
wynikx2.Text := '';
end;
else //jezeli c=0, to brak
begin
x1 := 0;
wynikx1.Text := FloatToStr(x1);
wynikx1.Text := ''; //wy
ś
wietlenie wyników
wynikx2.Text := '';
end;
mlewy := round(x1)-MARGIN; //ustalenie marginesu
mprawy := round(x1)+MARGIN;
end;
chart2LS.Clear; // czyszczenie wykresu
// rysowanie funkcji kwadratowej!
for i := mlewy to mprawy do
begin
x := i;
y := (a*x*x)+(b*x)+c;
chart2LS.AddXY(x, y); //rysowanie punktu na wykresie
end;
end;
end.
161
Rys. 10.14. Widok okna aplikacji rozwiązującej równanie kwadratowe
10.9.
Rysowanie figur geometrycznych
Zbudujemy aplikację, która pozwoli rysować figury geometryczne. Figury
można rysować w kilku komponentach, jednak najbardziej odpowiednim do te-
go celu jest komponent TShape.
Założenie: rysowanie figur geometrycznych na różne sposoby.
Rozwiązanie:
a)
rysowanie figur w komponencie TImage,
b)
rysowanie prostych figur w komponencie TShape,
c)
rysowanie złożonych figur w komponencie TShape,
Potrzebne będą komponenty: TRadioGroup x2, TImage, TShape x3, TPanel.
162
Rys. 10.15. Widok formularza aplikacji do rysowania figur
Złożone kształty (np. trójkąty o różnym położeniu) można uzyskać poprzez
odpowiednie przesłanianie i położenie komponentów Shape2, Shape3 i Panel:
1)
komponent Shape2 ustaw jako Deltoid, rozmiar (Height=100,
Width=200), kształt:
Shape=stsquaredDiamond,
2)
komponent Shape3 ustaw jako kwadrat: rozmiar (Height=100,
Width=100), kształt:
Shape=setsquare,
3)
komponenty Shape2 i Shape3 rozmieść tak, aby Shape3 zakrywało pra-
wą połowę komponentu Shape2,
4)
komponent Panel1: rozmiar (Height=50, Width=200), ustaw tak, by za-
krywał dolną połowę komponentów Shape2 i Shape3.
163
Rys. 10.16. Widok aplikacji w trakcie wyświetlania figur
Kod modułu (programu):
TForm1 = class(TForm)
Image1: TImage;
Panel1: TPanel;
RadioGroup1: TRadioGroup;
RadioGroup2: TRadioGroup;
Shape1: TShape;
Shape2: TShape;
Shape3: TShape; //
↓
konstruktor
procedure FormCreate(Sender: TObject);
procedure RadioGroup1Click(Sender: TObject);
procedure RadioGroup2Click(Sender: TObject);
private
{ private declarations }
public //
↓↓↓↓
punkty do rysowania trójk
ą
ta
trojkat: array [1..3] of TPoint;
x: Integer;
end;
var Form1: TForm1;
szer, wys: Integer;
kolor: Tcolor;
164
implementation
{ TForm1 }
// rysowanie figur w komponentach Image1 i Shape1
procedure TForm1.RadioGroup1Click(Sender: TObject);
begin //ustalenie wierzchołków trójk
ą
ta (dowolnie)
troj[1].X := 100; troj[1].Y := 100;
troj[2].X := 100; troj[2].Y := 0;
troj[3].X := 0; troj[3].Y := 100;
Image1.Canvas.Brush.Color := clWhite;
//Image1.Canvas.ClipRect;
Image1.Canvas.FillRect(Canvas.ClipRect); //czyszczenie
Shape1.Canvas.Brush.Color:=clWhite; // na biało
Shape1.Canvas.ClipRect;
Shape1.Canvas.FillRect(Canvas.ClipRect); //czyszczenie
//wybór figury do narysowania w Image1
//rysowanie koła
if Radiogroup1.ItemIndex=0 then
Image1.Canvas.Ellipse(100,100,0,0);
//rysowanie kwadratu
if Radiogroup1.ItemIndex=1 then
Image1.Canvas.Rectangle(100,100,0,0);
//rysowanie prostok
ą
ta
if Radiogroup1.ItemIndex=2 then
Image1.Canvas.Rectangle(100,150,0,0);
//rysowanie trójk
ą
ta
if Radiogroup1.ItemIndex=3 then
Image1.Canvas.Polygon(troj);
//wybór figury do narysowania w Shape1 poprz Canvas
//rysowanie koła
if Radiogroup1.ItemIndex=0 then
Shape1.Canvas.ellipse(100,100,0,0);
//rysowanie kwadratu
if Radiogroup1.ItemIndex=1 then
Shape1.Canvas.Rectangle(100,100,0,0);
//rysowanie prostok
ą
ta
if Radiogroup1.ItemIndex=2 then
Shape1.Canvas.RoundRect(100,150,0,0,15,15);
//rysowanie trójk
ą
ta
if Radiogroup1.ItemIndex=3 then
Shape1.Canvas.Polygon(troj);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin // shape2.Width:=100;
wys := shape2.Height;
165
szer := shape2.Width;
Shape3.Brush.color := clDefault; //kolor tla
kolor := Panel1.color;
// Shape3.Pen.color := clDefault;
// aby zaslaniajacy ksztalt nie byl widoczny
Panel1.visible := false;
end;
//rysowanie figur w komponentach Shape2 i Shape3
procedure TForm1.RadioGroup2Click(Sender: TObject);
begin
Shape2.Brush.Color := 123; //kolor br
ą
zowy
Shape3.visible := false;
Panel1.Visible := False;
//schowanie komponentu przyslaniaj
ą
cego Panel1
if Radiogroup2.ItemIndex=0 then
Shape2.shape := stCircle; //rysowanie koła
if Radiogroup2.ItemIndex=1 then
Shape2.shape := stSquare; //rysowanie kwadratu
if Radiogroup2.ItemIndex=2 then
Shape2.shape := stRoundSquare;
//rysowanie zaokr
ą
glonego kwadratu
if Radiogroup2.ItemIndex=3 then
begin
Shape2.shape := stRectangle; //rysowanie prostok
ą
ta
end;
if Radiogroup2.ItemIndex=4 then
Shape2.shape := stRoundRect;
//rysowanie zaokr
ą
glonego prostok
ą
ta
if Radiogroup2.ItemIndex=5 then
Shape2.shape := stEllipse;
//rysowanie elipsy
if Radiogroup2.ItemIndex=6 then
Shape2.shape := stDiamond;
//rysowanie deltoidu
if Radiogroup2.ItemIndex=7 then
begin
Shape2.shape := stSquaredDiamond;
//rysowanie trojkata przez zasloniecie
//polowy rombu przez kwadrat (Shape3)
Shape3.Pen.Style := psClear;//przezroczyste pioro
//Shape3.Pen.Color := kolor; ;
Shape3.shape := stSquare;
Shape3.visible := true;
end;
if Radiogroup2.ItemIndex=8 then
begin
Shape2.shape := stSquaredDiamond;
166
//rysowanie trojkata przez zasloniecie
//polowy rombu przez Panel1
Panel1.Visible := True;
end;
end;
end.
10.10.
Tablice i komponent TListBox
Pokażemy jak stworzyć aplikację, której celem jest praca z tablicą jedno-
wymiarową o określonym rozmiarze. Zadania szczegółowe:
•
generowanie danych losowych do tablicy (przycisk
Losuj – Button2),
•
wyświetlenie elementów tablicy (komponent
Listbox1),
•
wybór typu elementów tablicy (byte, Integer, float) (komponent Radio-
Group1),
•
zmiana zakresu losowanych danych (ComboBox1),
•
czyszczenie okna z danymi czyli komponentu listbox1 (przycisk –
Wy-
czysc – Button3),
•
dodanie liczby z okna typu TEdit do okna typu TListBox (przycisk –
Dodaj – Button1),
•
pomnożenie elementów tablicy przez liczbę i wyświetlenie ich w innym
oknie (komponent
memo1, przycisk Mnoz – Button4),
•
obliczenie średniej z elementów tablicy (przycisk
Srednia
−−−−
Button5
+Label1),
•
obliczenie sumy z elementów tablicy (przycisk
Srednia
−−−−
Button6
+Label1),
•
obliczenie największego elementu tablicy (przycisk
Max
−−−−
Button7
+Label1).
Rozwiązanie:
Rozmiar tablicy ustalono na 20. Typ elementów to Float, który może prze-
chować elementy typu
Integer i Byte. Aktualny rozmiar tablicy N ustalono na 10
(czyli będzie wyświetlana tablica 10-elementowa). Tablicę zadeklarowano jako
zmienną globalną modułu (przed sekcją
implementation). Zmienną x, do mno-
ż
enia tablicy, też zadeklarowano jako zmienną globalną.
167
Rys. 10.17. Widok formularza wyświetlania tablicy w komponencie ListBox
Rys. 10.18. Widok aplikacji
−
wyświetlanie tablicy i operacje na tablicy
Kod modułu:
unit Unit1;
{$mode objfpc}{$H+}
168
interface
uses
Classes, SysUtils, FileUtil, Forms, Controls, Graphics,
Dialogs, StdCtrls, ExtCtrls;
type { TForm1 }
TForm1 = class(TForm)
Button1: TButton; Button2: TButton;
Button3: TButton; Button4: TButton;
Button5: TButton; Button6: TButton;
Button7: TButton;
ComboBox1: TComboBox;
Edit1: TEdit;
Label1: TLabel;
ListBox1: TListBox;
Memo1: TMemo;
RadioGroup1: TRadioGroup;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure Button3Click(Sender: TObject);
procedure Button4Click(Sender: TObject);
procedure Button5Click(Sender: TObject);
procedure Button6Click(Sender: TObject);
procedure Button7Click(Sender: TObject);
private
public
end;
var Form1: TForm1;
A : Array[0..20] of float;
N : Integer = 9;
zakres : Integer = 10;
x : Integer;
implementation
{$R *.lfm} { TForm1 }
procedure TForm1.Button1Click(Sender: TObject);
begin
ListBox1.Items.Add(Edit1.Text)
end;
procedure TForm1.Button2Click(Sender: TObject);
var i: Byte;
begin
zakres:=StrtoInt(ComboBox1.Text);
169
for i:=0 to N do
begin
if RadioGroup1.ItemIndex=0 then
ListBox1.Items.Add(InttoStr(random(zakres)));
if RadioGroup1.ItemIndex=1 then
ListBox1.Items.Add(InttoStr(2*random(zakres)-zakres));
if RadioGroup1.ItemIndex=2 then
ListBox1.Items.Add(FloattoStr(2*random-1));
end;
end;
procedure TForm1.Button3Click(Sender: TObject);
begin
Memo1.Text :=ListBox1.Items.GetText;
ListBox1.Items.Clear();
end;
procedure TForm1.Button4Click(Sender: TObject);
var i: Byte;
begin
x := 1;
x := StrtoInt(Edit1.Text);
Memo1.Text := '';
for i:=0 to N do
begin
ListBox1.ItemIndex:=i;
A[i] := StrtoFloat
(ListBox1.Items.Strings[ListBox1.ItemIndex]);
Memo1.Lines.Add(FloattoStr(A[i]*x));
end;
end;
procedure TForm1.Button5Click(Sender: TObject);
var i: Byte;
max: Double;
begin
max := A[0];
for i:=0 to N do
if A[i]> max then max := A[i];
Label1.Caption := FloatToStr(max);
end;
procedure TForm1.Button6Click(Sender: TObject);
var i: Byte;
s: Double;
begin
170
s := 0;
for i:=0 to N do s := s+A[i];
Label1.Caption := FloatToStr(s/(N+1));
end;
procedure TForm1.Button7Click(Sender: TObject);
var i: Byte;
s: Double;
begin
s := 0;
for i:=0 to N do
begin
// ListBox1.ItemIndex := i;
// A[i] := StrtoFloat
// ListBox1.Items.Strings[ListBox1.ItemIndex];
s := s+A[i];
end;
Label1.Caption := FloatToStr(s);
end;
end.
10.11.
Okna dialogowe
Napiszemy aplikację, której celem jest praca z oknami dialogowymi.
Zadania szczegółowe:
•
pisanie tekstu w oknie tekstowym (w komponencie
Memo1),
•
zapisanie tekstu znajdującego się w Memo1 do pliku tekstowego
(za pomocą przycisku
Save – Button3 oraz komponentu SaveDialog1),
•
odczytanie tekstu z pliku i wpisanie go do okna
Memo1 (za pomocą
przycisku
Open – Button1 oraz komponentu OpenPictureDialog1),
•
odczytanie rysunku i pokazanie go w oknie graficznym (za pomocą
przycisku
Graph–Open – Button2 oraz Image1 i OpenDialog1),
•
zmiana koloru czcionki tekstu (za pomocą przycisku
Color – Button4
oraz komponentu
ColorDialog1),
•
zmiana formatu czcionki tekstu (za pomocą przycisku
Font – Button6
oraz komponentu
FontDialog1).
171
Rys. 10.19. Widok formularza aplikacji do edytowania tekstu
i wykorzystania okien dialogowych
Kod modułu:
unit Unit1;
{$mode objfpc}{$H+}
interface
uses Classes, SysUtils, FileUtil, Forms, Controls,
Graphics, Dialogs, StdCtrls, ExtCtrls, PopupNotifier,
ExtDlgs, SynEdit, PrintersDlgs;
type { TForm1 }
TForm1 = class(TForm)
Button1: TButton; Button2: TButton;
Button3: TButton; Button4: TButton;
Button5: TButton; Button6: TButton;
ColorDialog1: TColorDialog;
FontDialog1: TFontDialog;
Image1: TImage;
Memo1: TMemo; Memo2: TMemo;
OpenDialog1: TOpenDialog;
OpenPictureDialog1: TOpenPictureDialog;
PrintDialog1: TPrintDialog;
SaveDialog1: TSaveDialog;
172
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure Button3Click(Sender: TObject);
procedure Button4Click(Sender: TObject);
procedure Button5Click(Sender: TObject);
procedure Button6Click(Sender: TObject);
private
public
{ public declarations }
end;
var Form1: TForm1;
implementation
{$R *.lfm} { TForm1 }
procedure TForm1.Button1Click(Sender: TObject);
begin //uzycie OpenDialog
if OpenDialog1.Execute then
Memo1.Lines.LoadFromFile(OpenDialog1.FileName);
end;
procedure TForm1.Button2Click(Sender: TObject);
begin //ustawienie filtru
OpenPictureDialog1.Filter:='*.bmp';
if OpenPictureDialog1.Execute then //załadowanie obrazu
Image1.Picture.LoadFromFile
(OpenPictureDialog1.FileName);
end;
procedure TForm1.Button3Click(Sender: TObject);
begin
if SaveDialog1.Execute then
Memo1.Lines.SaveToFile(SaveDialog1.FileName);
end;
procedure TForm1.Button4Click(Sender: TObject);
begin
if colorDialog1.Execute then
Memo1.Color:=ColorDialog1.Color;
end;
procedure TForm1.Button5Click(Sender: TObject);
begin
if PrintDialog1.Execute then Memo2.Text:='Drukuj';
// PrintDialog1.Print:=Memo1.Lines.ToString;
end;
173
procedure TForm1.Button6Click(Sender: TObject);
begin
if FontDialog1.Execute then
Memo1.Font:=FontDialog1.Font;
end;
end.
10.12.
Tablice jednowymiarowe i komponent TStringGrid
Napiszemy aplikację, do pracy z tablicą jednowymiarową, przy użyciu
komponentu
TSringGrid.
Zadania szczegółowe:
•
generowanie danych losowych do tablicy (przycisk
Losowo-Button1);
•
wyświetlenie elementów tablicy (komponent StringGrid1)
•
obliczenie sumy z elementów tablicy (przycisk
Suma – Button2 + La-
bel1)
•
obliczenie średniej arytmetycznej elementów tablicy (przycisk
Srednia
– Button3 + Label2)
•
obliczenie największego elementu tablicy (komponent LabelEdit1)
•
zmiana koloru siatki (komponent CheckBox1)
Rozwiązanie:
Rozmiar tablicy ustalono na 10 (N = 9, indeksowanie od 0). Typ elementów
to
Integer. Tablicę zadeklarowano jako pole klasy. Tablicę jednowymiarową
można wyświetlić w komponencie StringGrid (siatka) w poziomie (kolumnowo)
lub w pionie (wierszowo). W tej aplikacji tablica będzie wyświetlana w jednej
kolumnie. Jest to wynik ustawienia parametru RowCount=11 (10+1) - licznik
wierszy. Dodatkowa „jedynka” to liczba wierszy nagłówkowych (ustalonych pa-
rametrem FixedRows =1).
Drugi rozmiar siatki StringGrid1 to parametr ColCount=2 (1+1) - licznik
kolumn. Dodatkowa „jedynka” to liczba kolumn nagłówkowych (ustalonych pa-
rametrem FixedCols=1). W ustalonych kolumnach i wierszach można wstawiać
np. podpisy kolumn/wierszy, numery kolumn/wierszy itp.
174
Rys. 10.20. a) Widok formularza do generowania i wyświetlania tablicy,
b) widok działającej aplikacji
Kod modułu:
unit Unit1;
{$mode objfpc}{$H+}
interface
uses Classes, SysUtils, FileUtil, Forms, Controls,
Graphics, Dialogs, StdCtrls, Grids, Buttons, ExtCtrls;
const N=9;
type { TForm1 }
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
Button3: TButton;
CheckBox1: TCheckBox;
Label1: TLabel;
Label2: TLabel;
LabeledEdit1: TLabeledEdit;
StringGrid1: TStringGrid;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure Button3Click(Sender: TObject);
procedure CheckBox1Change(Sender: TObject);
procedure FormCreate(Sender: TObject);
175
procedure LabeledEdit1Click(Sender: TObject);
public
wektor:array[0..N]of integer;
end;
var Form1: TForm1;
wektor1 : array [0..N] of Integer;
implementation
{$R *.lfm} { TForm1 }
procedure TForm1.FormCreate(Sender: TObject);
var i : Byte;
begin
for i:=0 to N do //numerowanie wierszy
StringGrid1.Cells[0,i+1] := Inttostr(i+1);
// Cels[k,w] k=kolumny w=wiersze
end;
procedure TForm1.Button1Click(Sender: TObject);
var i : Integer;
begin
Randomize;
for i := 0 to N do
wektor[i] := random(21)-10; //wpis do wektora
//przepisanie danych tablicy do siatki
for i:=0 to N do
StringGrid1.Cells[1,i+1]:=Inttostr(wektor[i]);
end;
procedure TForm1.Button2Click(Sender: TObject);
var suma, i : Integer;
begin
suma:=0;
for i:=0 to N do
begin
wektor[i]:=StrToInt(StringGrid1.Cells[1,i+1]);
suma:=suma+wektor[i];
end;
Label1.Caption:='suma='+IntToStr(suma);
end;
procedure TForm1.Button3Click(Sender: TObject);
var suma, i : Integer;
begin
suma := 0;
for i := 0 to N do
176
begin
wektor[i] := StrToInt(StringGrid1.Cells[1,i+1]);
suma := suma+wektor[i];
end;
Label2.Caption:='srednia='+FloatToStr(suma/(N+1));
end;
procedure TForm1.LabeledEdit1Click(Sender: TObject);
var max, i : Integer;
begin
max := wektor[0];
for i := 0 to N do
begin
wektor[i] := StrToInt(StringGrid1.Cells[1,i+1]);
if wektor[i] > max then max := wektor[i];
end;
LabeledEdit1.text:=IntToStr(max);
end;
procedure TForm1.CheckBox1Change(Sender: TObject);
begin
if CheckBox1.Checked := True then
StringGrid1.Color := clYellow
else StringGrid1.Color := clDefault;
end;
end.
10.13.
Tablice dwuwymiarowe i komponent TStringGrid
Napiszemy aplikację, do pracy z tablicą dwuwymiarową, przy użyciu kom-
ponentu
TSringGrid.
Zadania szczegółowe:
•
generowanie danych losowych do tablicy (przycisk
Losowo:Button1),
•
wyświetlenie elementów tablicy (komponent StringGrid1),
•
obliczenie sumy elementów tablicy (przycisk
Suma:Button2 +Label1),
•
obliczenie średniej arytmetycznej elementów tablicy (przycisk
Srednia:
Button3
+ Label2),
•
obliczenie największego elementu tablicy (komponent LabelEdit1),
•
zamykanie aplikacji (przycisk
Koniec: Button4).
Rozwiązanie:
177
Rozmiar tablicy ustalono na 10x10 (rM=10). Typ elementów
−
na
Integer.
Tablicę zadeklarowano jako pole klasy. Aktualny rozmiar tablicy N = 4 ustalono
w konstruktorze (
FormCreate). Ustawiono parametr RowCount = 11 (10+1) -
licznik wierszy oraz parametr ColCount = 11 (10+1) - licznik kolumn. Kolumna
i wiersz nagłówkowy: parametr FixedCols = 1 oraz FixedRows = 1.
Rys. 10.21. Widok formularza aplikacji do generowania i wyświetlania macierzy
Kod modułu:
unit Unit1;
{$mode objfpc}{$H+}
interface
uses Classes, SysUtils, FileUtil, Forms, Controls,
Graphics, Dialogs, StdCtrls,Grids, Buttons, ExtCtrls;
const rM = 10;
type { TForm1 }
TForm1 = class(TForm)
Button1: TButton; Button2: TButton;
Button3: TButton; Button4: TButton;
Label1: TLabel; Label2: TLabel;
LabeledEdit1: TLabeledEdit;
StringGrid1: TStringGrid;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure Button3Click(Sender: TObject);
procedure Button4Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure LabeledEdit1Change(Sender: TObject);
178
procedure LabeledEdit1Click(Sender: TObject);
private
{ private declarations }
public
macierz : array [1..rM,1..rM] of Integer;
N : Byte; //rozmiar siatki
end;
var Form1: TForm1;
implementation
{$R *.lfm}
{ TForm1 }
procedure TForm1.FormCreate(Sender : TObject);
var i : Byte;
begin
N := 4;
for i := 1 to N do //numerowanie wierszy
StringGrid1.Cells[0,i] := IntToStr(i);
for i := 1 to N do //numerowanie kolumn
StringGrid1.Cells[i,0] := IntToStr(i);
// Cels[k,w] k=kolumny w=wiersze
end;
procedure TForm1.Button1Click(Sender : TObject);
var i, j : Integer;
begin
Randomize;
for i:=1 to N do
for j:=1 to N do
macierz[i,j] := random(21)-10; //wpis do wektora
// przepisanie danych z macierzy do siatki
for i:=1 to N do
for j:=1 to N do
StringGrid1.Cells[j,i] := IntToStr(macierz[i,j]);
end;
procedure TForm1.Button2Click(Sender : TObject);
var suma, i, j : Integer;
begin
suma := 0;
for i := 1 to N do
for j := 1 to N do
begin
macierz[i,j] := StrToInt(StringGrid1.Cells[j,i]);
179
suma := suma+macierz[i,j];
end;
Label1.Caption := 'suma=' + IntToStr(suma);
end;
procedure TForm1.Button3Click(Sender : TObject);
var suma, i, j : Integer;
begin
suma := 0;
for i := 1 to N do
for j := 1 to N do
begin
macierz[i,j] := StrToInt(StringGrid1.Cells[j,i]);
suma := suma+macierz[i,j];
end;
Label2.Caption := 'srednia='+FloatToStr(suma/(N*N));
end;
procedure TForm1.LabeledEdit1Click(Sender : TObject);
var max, i, j : Integer;
begin
max := macierz[1,1];
for i := 1 to N do
for j := 1 to N do
begin
macierz[i,j]:=StrToInt(StringGrid1.Cells[j,i]);
if macierz[i,j]> max then
begin
max := macierz[i,j];
StringGrid1.GridLineColor := clYellow;
end
else
StringGrid1.GridLineColor := clGray;
end;
LabeledEdit1.text := IntToStr(max);
end;
procedure TForm1.Button4Click(Sender: TObject);
begin
Close;
end;
end.
180
10.14.
Zadania
Zad. 10.1. Zbudować aplikację Silnia, która będzie obliczać wartość silni dla
podanej liczby naturalnej (użyć komponentów Button, EditBox i Label).
Zad. 10.2. Zbudować aplikację Licznik, która będzie zliczać do przodu kliknięcia
na przycisku1 oraz zliczać do tyłu kliknięcia na przycisku2. W oknie
edycyjnym ma być możliwość ustalenia krotności zliczania (od 1 do 10).
Zad. 10.3. Zbudować aplikację Potęgowanie, która będzie obliczać wartość po-
tęgi danej liczby: a) wersja podstawowa, czyli tylko potęga całkowita
dodatnia; b) potęga całkowita ujemna; c) potęga rzeczywista.
Zad. 10.4. Zbudować aplikację Stoper,
a)
która wyświetla różnicę czasu aktualnego i czasu startu aplikacji wyko-
rzystując funkcję
Time lub Now. Program ma posiadać dwa przyciski:
START/STOP oraz RESET.
b)
po 10 sekundach od startu stopera cyfry zmieniają stan co 200ms za-
miast co 1 sekundę, a nagłówek programu zmienia się na „Stoper przy-
spieszył”.
c)
w oknie głównym pokazuje aktualny czas (wskazówka: przydatna bę-
dzie funkcja
Now (lub funkcja Time) oraz TimeToStr).
d)
po 10 sekundach od startu stopera na płótnie formy ma pojawiać się ko-
ło z czarnym brzegiem i wnętrzem migającym na czerwono-czarno, co
sekundę. Użyć właściwości
Canvas, najlepiej dla komponentu Image,
ewentualnie dla głównej formy programu.
e)
wielkość cyfr zmienia się stosownie do zmiennych rozmiarów okna
aplikacji. Wskazówki: aby okno mogło zmieniać rozmiary, trzeba wła-
ś
ciwość BorderStyle ustawić na bsSizeable. Użyć w programie właści-
wości ClientHeight, ClientWidth.
Zad. 10.5. Zbudować kalkulator do obliczeń na liczbach rzeczywistych.
Zad. 10.6. Zbudować kalkulator do obliczeń na liczbach zespolonych.
Zad. 10.7. Zbudować kalkulator do obliczeń na macierzach kwadratowych.
Zad. 10.8. Zbudować aplikację dokładnego dodawania dużych liczb naturalnych.
Zad. 10.9. Zbudować aplikację, która będzie prostym edytorem tekstu.
Zad. 10.10. Zbudować aplikację, która będzie rysować różne figury: trójkąt,
kwadrat, prostokąt, trapez, romb, równoległobok, sześciokąt. Rozmiary
figur podawane mają być w oknach typu TextBox. Aplikacja ma liczyć
obwód i pole figury.
Zad. 10.11. Zbudować aplikację do konwersji liczb na różne systemy liczbowe
(dziesiętny, binarny, szesnastkowy, ósemkowy itp.).
181
LITERATURA
[1]
Cantu M.: Delphi 7. Praktyka programowania. Mikom, Warszawa, 2004.
[2]
Gierliński M.: Pascal – nawet Ty możesz programować. Wydawnictwo Edi-
tion 2000, Kraków, 1998.
[3]
Kierzkowski A.: Turbo Pascal. Ćwiczenia praktyczne. Helion, Gliwice,
2000.
[4]
Kott Ryszard K.: Programowanie w języku Pascal. WNT, Warszawa, 1988.
[5]
Marciniak A.: Object Pascal – język programowania w środowisku Borland
Delphi 2.0. Nakom, Poznań, 1997.
[6]
Porębski W.: Pascal. Wprowadzenie do programowania. Komputerowa
Oficyna Wydawnicza „HELP”, Michałowice pod Warszawą, 1999.
[7]
Stephens R.: Algorytmy i struktury danych z przykładami w Delphi. Helion,
Gliwice, 2000.
[8]
Struzińska-Walczak A., Walczak K.: Nauka programowania dla … już nie
całkiem początkujących. Wydawnictwo W&W, Warszawa, 2004.
[9]
Struzińska-Walczak A., Walczak K.: Nauka programowania w systemie
Delphi. Wydawnictwo W&W, Warszawa, 2004.
[10]
Strzałkowski K.: Podstawy Delphi. Wydawnictwo Stachurski, Kielce, 2000.
[11]
http://wazniak.mimuw.edu.pl/index.php?title=Wst%C4%99p_do_progra
mowania
[12]
http://wazniak.mimuw.edu.pl/index.php?title=Paradygmaty_progra
mowania
[13]
http://wazniak.mimuw.edu.pl/index.php?title=Matematyka_dyskretna_1
[14]
http://wiki.freepascal.org/Main_Page
[15]
http://wiki.freepascal.org/Object_Pascal_Tutorial
[16]
http://www.tutorialspoint.com/pascal/index.htm
[17]
http://turbopascal.helion.pl
[18]
http://www.pascal.vj.e.pl
[19]
http://edu.i-lo.tarnow.pl/inf/utils/010_2010/index.php
[20]
http://javablock.sourceforge.net/
[21]
http://www.pm.waw.pl/~marwoj/javablock/javablock.pdf
[22]
http://lobrzozow.internetdsl.pl/materialy/ti/Ti2.htm