![]() |
Turbo Pascal.
|
![]() |
Funkcje i procedury
Jak w życiu, tak i w programowaniu obowiązuje zasada, że nie należy robić tego samego więcej niż raz. Jeśli np. musisz kilkakrotnie obliczyć w programie wartość jakiegoś skomplikowanego wyrażenia, to oczywiście możesz to zrobić przepisując (lub powielając za pomocą operacji blokowych) odpowiednie fragmenty programu, ale trudno powiedzieć, by było to najlepsze rozwiązanie. Po pierwsze, wymaga to trochę pracy. Po drugie, program robi się dłuższy, mniej elastyczny i mniej czytelny (o elegancji nie mówiąc). Po trzecie, jakakolwiek poprawka w treści wyrażenia musi zostać wprowadzona do wszystkich powielonych fragmentów; jeśli zapomnisz choćby o jednym, wyniki mogą być opłakane.
Powyższe argumenty w wystarczającym stopniu uzasadniają potrzebę wprowadzenia rozwiązania umożliwiającego "ukrycie" wybranej operacji (lub grupy operacji) w pojemniku, do którego dostarczalibyśmy dane i odbierali wynik bez przejmowania się sposobem przetwarzania informacji wewnątrz. Rozwiązaniem takim są funkcje i procedury. Obydwa pojęcia są Ci już znane - wielokrotnie wykorzystywałeś procedury i funkcje biblioteczne Pascala (np. writeln czy exp) bez zastanawiania się nad zasadą ich działania. Okazuje się jednak, że w nieskomplikowany sposób możesz również tworzyć własne procedury i funkcje, co pozwoli Ci uniknąć prowizorki, jaką zaprezentowaliśmy w poprzednim rozdziale.
Czym jest funkcja? Z punktu widzenia programu, który ją wykorzystuje, jest to rodzaj "czarnej skrzynki", przetwarzającej włożone do niej informacje i zwracającej odpowiedni wynik. Z punktu widzenia programisty natomiast funkcja jest swoistym "miniprogramem" - zestawem zamkniętych w logiczną całość instrukcji. Instrukcje te mogą zajmować się wykonywaniem dowolnych operacji, chociaż na ogół ich celem jest przekształcenie wprowadzonych do funkcji informacji (tzw. parametrów) do żądanej postaci, zwracanej następnie poprzez nazwę funkcji. Procedura różni się od funkcji jedynie sposobem zwracania wyniku, o czym powiemy za chwilę.
Co należy zrobić, żeby wykorzystać w programie funkcję lub procedurę? Po pierwsze trzeba ją zdefiniować, a następnie wywołać. Zajmijmy się na początek definicją, która polega na:
- określeniu sposobu kontaktowania się jej z otoczeniem (ustaleniu nazwy, zestawu parametrów i typu zwracanego wyniku), oraz
- określeniu jej zawartości (czyli zestawu tworzących ją instrukcji i pomocniczych obiektów lokalnych, o których później).
A oto składnia definicji funkcji3:
function nazwa-funkcji(lista-parametrów):typ-wyniku; deklaracje-i-definicje-obiektów lokalnych begin instrukcje-realizujące-treść funkcji end; procedure nazwa-procedury(lista-parametrów); deklaracje-i-definicje-obiektów-lokalnych begin instrukcje-realizujące-treść-procedury end; Definicja rozpoczyna się tak zwanym nagłówkiem, sygnalizowanym słowem kluczowym function lub procedure, po którym następuje umieszczona w nawiasach okrągłych lista parametrów (argumentów) formalnych. Parametry te symbolizują wartości, które zostaną przekazane do funkcji i przetworzone, zaś sama lista ma postać:
lista-nazw : typ; lista-nazw : typ; ...
czyli zawiera zestawy nazw parametrów tego samego typu rozdzielone średnikami (w obrębie zestawu nazwy parametrów rozdzielone są przecinkami). Warto zauważyć, że lista parametrów nie jest obowiązkowym elementem definicji, tak więc istnieje możliwość zdefiniowania funkcji bezparametrowej, nie pobierającej informacji z otoczenia. Ostatnim elementem nagłówka jest określenie typu zwracanego wyniku, wymagane wyłącznie dla funkcji.
Sama treść funkcji zdefiniowana jest pomiędzy słowami kluczowymi begin i end i może być poprzedzona deklaracjami i definicjami tak zwanych obiektów lokalnych, wykorzystywanych przez funkcję do użytku wewnętrznego. Obiektami lokalnymi zajmiemy się szerzej w dalszej części książki. Same instrukcje składające się na treść definicji funkcji nie różnią się niczym od używanych w "zwykłym" programie, z wyjątkiem tego, że w przypadku funkcji należy umieścić w definicji instrukcję przypisania
nazwa-funkcji := wartość-wyniku
umożliwiającą przekazanie obliczonego wyniku "na zewnątrz". Co ciekawe, obecność tej instrukcji w treści funkcji nie jest sprawdzana przez kompilator (jak ma to miejsce np. dla kompilatorów języka C), toteż zapominalskim programistom może się przytrafić zdefiniowanie funkcji kompletnie nie potrafiącej skomunikować się z otoczeniem (nie pobierającej żadnych parametrów i nie zwracającej wyniku), a więc praktycznie bezużytecznej. Warto zdawać sobie sprawę, że wywołanie funkcji pozbawionej instrukcji zwrócenia wyniku da w rezultacie wartość nieokreśloną (przypadkową), co raczej nie może być uznane za efekt pozytywny.
Jak widać z powyższych zapisów, definicja funkcji lub procedury przypomina mały program. Definicje wszystkich funkcji i procedur wykorzystywanych w programie muszą być umieszczone przed miejscem ich wywołania. To ostatnie może nastąpić w części operacyjnej programu (a zatem definicje muszą poprzedzać rozpoczynające właściwy program słowo begin) lub w treści innej funkcji lub procedury (co wymusza odpowiednie ułożenie definicji względem siebie4). Struktura programu wykorzystującego funkcje i procedury powinna więc wyglądać następująco:
nagłówek-programu deklaracje-i-definicje-obiektów-globalnych definicje-funkcji-i-procedur begin część-operacyjna-programu end. Umieszczanie deklaracji i definicji obiektów globalnych (o których również powiemy później) przed definicjami funkcji nie jest co prawda obowiązkowe (chyba, że obiekty te są wykorzystywane w funkcjach), stanowi jednak element dobrej praktyki programistycznej. Natomiast jednym z pospolitych błędów popełnianych przez początkujących programistów jest próba umieszczania definicji funkcji i procedur w części operacyjnej programu, co jest oczywiście nielegalne.
Samo zdefiniowanie funkcji nic nam jeszcze nie daje (jego efektem jest wyłącznie utworzenie odpowiedniego fragmentu kodu wynikowego). Dopiero wywołanie funkcji powoduje wykonanie składających się na jej treść instrukcji i rzeczywiste przekształcenie informacji podanej w postaci parametrów. Aby wywołać procedurę lub funkcję, wystarczy umieścić w programie jej nazwę wraz z listą parametrów aktualnych, czyli wyrażeń zawierających informację, która ma być przetworzona. Zawartość listy parametrów aktualnych musi odpowiadać liście podanej w definicji funkcji. W szczególności, jeśli w definicji pominięto listę parametrów, nie podaje się ich podczas wywołania (mamy wówczas do czynienia z funkcją lub procedurą bezparametrową).
Wywołanie procedury ma postać
nazwa-procedury(lista-parametrów-aktualnych);
zaś w przypadku funkcji, która zwraca obliczoną wartość poprzez swoją nazwę, musisz dodatkowo zadbać o umieszczenie gdzieś zwróconego wyniku, np. tak:
zmienna := nazwa-funkcji(lista-parametrów-aktualnych);
Funkcję możesz również wykorzystać jako element wyrażenia lub instrukcji (np. writeln); ogólnie, możesz ją zastosować wszędzie tam, gdzie możliwe jest użycie wyrażenia5.
Począwszy od wersji 6.0 Turbo Pascal umożliwia Ci zignorowanie wyniku zwracanego przez funkcję, czyli wykorzystanie jej tak samo, jak procedury. Jest to możliwe po włączeniu opcji kompilatora Extended Syntax lub użyciu odpowiadającej jej dyrektywy {$X+}. Uff. Po tej solidnej (i nużącej) dawce teorii czas na nieco praktyki. Na początek spróbujmy zaradzić problemowi przedstawionemu pod koniec poprzedniego rozdziału. Oto poprawiona wersja programu Bisekcja, wykorzystująca funkcję
program Bisekcja; { Program rozwiązuje równania nieliniowe metodą bisekcji } var a, b, c : real; { granice przedziału i punkt podziału } eps : real; { dokładność } function f(x:real):real; { badana funkcja } begin f := 1 - exp(sin(x)*cos(x)); { treść funkcji } end; begin writeln('Program znajduje miejsce zerowe funkcji') writeln('w przedziale [a; b]'); write('Podaj wartosc a: '); { wprowadź granice przedzialu } readln(a); write('Podaj wartosc b: '); readln(b); write('Podaj dokladnosc: '); readln(eps); repeat c := (a + b)/2; { podziel przedział na pół } if (f(a)*f(c)) < 0 then { wygląda ładniej, prawda? } b := c { funkcja ma przeciwne znaki w a i c } else a := c; { funkcja ma przeciwne znaki w b i c } writeln(c); until abs(f(c)) < eps; { badamy wartość bezwzględną! } writeln('Miejsce zerowe: c = ',c:12:8); readln; end. Czym różni się nasz program od wersji poprzedniej? Po pierwsze, definicja badanej funkcji została wyodrębniona jako oddzielny fragment, który łatwo znaleźć, zinterpretować i poprawić. Po drugie, treść samego programu jest czytelniejsza (program zajmuje się teraz wyłącznie realizacją właściwego algorytmu, czyli szukaniem pierwiastka pewnej funkcji f(x). Po trzecie, zmniejszyła się podatność programu na błędy (zmiana definicji funkcji wymaga poprawienia tylko jednej linijki; zastanów się, jak wyglądałaby poprzednia wersja programu, gdyby obliczenia wymagały użycia kilku oddzielnych instrukcji). Wreszcie po czwarte, program wygląda ładniej, jest bardziej elegancki i wymaga mniej pisania.
Korzystanie z procedur wygląda bardzo podobnie, z wyjątkiem tego, że nie musisz się troszczyć o zagospodarowanie zwracanej wartości (ponieważ jej nie ma). Ogólnie rzecz biorąc, funkcje wykorzystywane są głównie tam, gdzie zachodzi potrzeba obliczenia jakiejś pojedynczej wartości i przekazania jej do kolejnych instrukcji programu, natomiast procedury pozwalają na efektywne wykonywanie powtarzających się, rutynowych czynności. Aby na przykład uatrakcyjnić szatę graficzną programu, możesz wykorzystać w nim następującą procedurę:
procedure Szlaczek; begin for i := 1 to 60 do { wypisz szlaczek } write('*'); { złożony z 60 gwiazdek } writeln; { przejdź do nowego wiersza } end; Gdzie umieścić definicję procedury - już wiesz. Pozostaje ją wywołać, co robi się bardzo prosto:
Szlaczek;
Każde takie wywołanie umieści na ekranie "szlaczek" składający się z 60 gwiazdek. Oczywiście, powyższa procedura nie jest idealna, bo jeśli np. zachce Ci się wypisać szlaczek złożony z kresek, będziesz musiał zmienić jej treść. Znacznie lepszym rozwiązaniem jest coś takiego:
procedure Szlaczek(Znak:char; Ile:byte); begin for i := 1 to Ile do { wypisz szlaczek } write(Znak); { złożony z iluś znaków } writeln; end; Spróbujmy w ramach zabawy wykorzystać naszą procedurę do wyświetlenia na ekranie trójkąta złożonego z gwiazdek, czyli czegoś takiego:
*
**
***
****itd. Odpowiedni program będzie wyglądał tak:
program Szlaczki; var i : integer; { licznik wierszy } procedure Szlaczek(Znak : char; Ile : integer); begin for i := 1 to Ile do { wypisz szlaczek } write(Znak); { złożony z iluś znaków } writeln end; begin for i := 1 to 20 do { wypisz 20 szlaczków } Szlaczek('*', i); end. Jeśli uważasz, że wszystko jest OK, spróbuj teraz zmienić program tak, by każdy szlaczek miał długość równą kolejnej liczbie nieparzystej, tj. pierwszy 1, drugi 3 itd. Ogólnie długość szlaczka wyrazi się wzorem Ile = 2
numer - 1, gdzie numer jest numerem wiersza, tak więc wystarczy zmienić główną pętlę programu na
for i := 1 to 20 do { wypisz 20 szlaczków } Szlaczek('*', 2*i - 1); Hm... no, chyba nie całkiem to chciałeś uzyskać, prawda? Efekt, którego doświadczyłeś uruchamiając program wynika z użycia w procedurze i wywołującej ją pętli tej samej zmiennej i, a wygląda następująco:
Wartość Przebieg pętli 2 1 3ma być 2 1 3jest przed procedurą 2 1 3jest po procedurze 3 1 7jest po inkrementacji licznika 4 2 8Każde wywołanie procedury powoduje modyfikację zmiennej i, która jest jednocześnie licznikiem pętli w programie głównym. Aby tego uniknąć, należałoby w procedurze użyć jako licznika pętli innej zmiennej. To z kolei nakłada na programistę obowiązek przejrzenia wszystkich procedur znajdujących się w programie i wcześniejszego zadeklarowania odpowiednich zmiennych tak, by się nie "nakładały". W efekcie otrzymujemy kolejny zestaw łatwych do przeoczenia pułapek. Rozwiązaniem tego problemu są zmienne lokalne, którymi jednak zajmiemy się już w następnym rozdziale. Tam również dokończymy omawianie przekazywania parametrów i wartości z i do procedur i funkcji.
Zapamiętaj
- Procedury i funkcje pascalowe pozwalają zamknąć dany zestaw operacji w logiczną całość, pobierającą z otoczenia odpowiednie informacje i zwracającą żądany wynik.
- Przed wykorzystaniem (wywołaniem) funkcji lub procedury należy ją zdefiniować, czyli określić jej treść i sposób komunikowania się z otoczeniem.
- Do przekazywania informacji do funkcji (procedury) służą parametry (argumenty). W trakcie definiowania informacja przekazywana do funkcji (procedury) symbolizowana jest parametrami formalnymi.
- Definicje funkcji (procedur) muszą być umieszczone w programie przed częścią operacyjną i muszą występować w odpowiedniej kolejności.
- Samo wywołanie funkcji (procedury) odbywa się przez podanie w programie jej nazwy uzupełnionej odpowiednią listą parametrów aktualnych.
- Stosowanie funkcji i procedur jest korzystne ze względu na poprawę czytelności i efektywności programu, zmniejszenie podatności na błędy i skrócenie czasu potrzebnego na jego pisanie.
Przypisy
3. Dla uproszczenia, w dalszym ciągu naszych rozważań będziemy używali słowa "funkcja" zarówno na określenie funkcji, jak i procedury. Ewentualne różnice będą wyraźnie zaznaczane. |
|
4. Ograniczenie to można obejść, wykorzystując tzw. deklaracje zapowiadające, rozpoczynane słowem kluczowym forward. |
|
5. Pamiętaj, że identyfikator funkcji użyty w programie nie jest l-wartością, a więc nie możesz używać funkcji tak samo, jak zmiennej - nie możesz np. przypisywać jej wartości (możliwe to jest wyłącznie w obrębie definicji funkcji i ma wówczas nieco inne znaczenie). |
|
Poprzedni | Spis treści | Następny | Wersja spakowana |