Turbo Pascal. Programowanie

Turbo Pascal.
Programowanie

Autor: Tomasz M. Sadowski
Format: B5, stron: 136

Copyright © 1996 by Wydawnictwo Helion
wersja spakowana

helion.pl

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:

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
3
ma być
2
1
3
jest przed procedurą
2
1
3
jest po procedurze
3
1
7
jest po inkrementacji licznika
4
2
8

Każ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

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. | wróć |

4. Ograniczenie to można obejść, wykorzystując tzw. deklaracje zapowiadające, rozpoczynane słowem kluczowym forward. | wróć |

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). | wróć |

Poprzedni | Spis treści | Następny | Wersja spakowana |