9. PODPROGRAMY - FUNKCJE I PROCEDURY
9.1. Konieczność stosowania podprogramów
Podprogram to niewielki program, który zostaje uruchomiony w jakimś miejscu programu głównego po to, aby wykonać określone zadanie cząstkowe. Mówimy, że podprogram jest wywołany w programie głównym. W momencie wywołania podprogram pobiera z programu głównego dane wejściowe, a po wykonaniu swojego zadania przekazuje do programu głównego dane wyjściowe (wyniki). W Turbo Pascalu istnieją dwa rodzaje podprogramów: funkcje i procedury
Są co najmniej cztery powody, dla których stosowanie podprogramów jest niezbędne:
W przypadku złożonych zadań, jest oczywista potrzeba dekompozycji zadania na stosunkowo proste, kolejno wykonywane, zadania cząstkowe. Wykonanie tych zadań cząstkowych powierza się podprogramom. Jest to tak zwane programowanie strukturalne. Dzięki niemu pisanie programów jest o wiele łatwiejsze; wygodniejsza jest też analiza programów, ich modyfikacja i testowanie. Zaprogramowanie bardzo złożonych zadań bez rozbicia programu na podprogramy byłoby praktycznie niemożliwe.
Użycie podprogramów poprawia efektywność wykorzystania pamięci operacyjnej. Program główny wymaga wtedy niewielkiej liczby zmiennych. Ogromna większość zmiennych jest wykorzystywana przez podprogramy jako t. zw. zmienne lokalne. Zmienne lokalne zajmują mało pamięci, bo istnieją tylko w czasie działania podprogramu, a następnie zwalniają pamięć do dyspozycji kolejnych podprogramów.
Jeżeli w programie powtarza się wielokrotnie ta sama sekwencja instrukcji, to jednokrotne zapisanie jej w postaci podprogramu znacznie skraca zapis programu źródłowego.
Raz opracowany podprogram może być wykorzystany jako podstawowy element funkcjonalny, który może być w miarę potrzeby użyty w dowolnym programie. Można nawet tworzyć własne biblioteki często używanych typowych procedur i funkcji dla różnych obszarów zastosowań - w Turbo Pascalu są one nazywane modułami własnymi.
9.2. Procedury i funkcje standardowe
Turbo Pascal oferuje programistom obszerny zbiór gotowych podprogramów, t. zw. procedur i funkcji standardowych, które mogą być wywoływane za pomocą odpowiednich instrukcji, zwanych instrukcjami wywołania. W dotychczas pokazywanych przykładach programów stanowiły one większość - jedynie nieliczne instrukcje podstawowe: while, repeat, for, break, continue, if, case, nie były instrukcjami wywołania.
Procedury i funkcje znajdują się w tak zwanych modułach o nazwach System, Crt, Dos, Windos, Graph, Overlay, Strings. Chcąc użyć procedury lub funkcji, zawartej w danym module, trzeba na początku programu zadeklarować użycie tego modułu, na przykład:
uses Crt,Dos;
Nie deklaruje się jednak podstawowego modułu System, którego zasoby są dołączane do programu domyślnie.
9.3. Wywołania funkcji
Funkcję wywołujemy, podając jej nazwę i po niej, w nawiasach zwykłych, argumenty, oddzielone przecinkami. Wywołania funkcji mogą być umieszczane:
w wyrażeniach - na ogół po prawej stronie instrukcji przypisania, lub w wyrażeniu relacyjnym
jako argument innej funkcji lub procedury.
W przykładzie 9.1 funkcja Sqr jest wywołana dwukrotnie dla różnych argumentów w wyrażeniu: Sqr(X1-X2)+Sqr(Y1-Y2). To wyrażenie jest z kolei argumentem innej funkcji Sqrt, która oblicza szukaną odległość i przekazuje wynik do zmiennej D.
Zwróćmy uwagę na to, że można by sobie poradzić bez zmiennej D, wpisując odpowiednie wyrażenie jako argument instrukcji Write, co pokazano poniżej:
Write('Odleglosc=',Sqrt(Sqr(X1-X2)+Sqr(Y1-Y2)));
Przykład 9.1. Obliczenie odległości między dwoma punktami na płaszczyźnie XY
program Ex9_1; uses Crt; var D,X1,Y1,X2,Y2:Real; begin Clrscr; Write('Podaj X1,Y1: '); Readln(X1,Y1); Write('Podaj X2,Y2: '); Readln(X2,Y2); D:=Sqrt(Sqr(X1-X2)+Sqr(Y1-Y2)); {Wywołania funkcji Sqr oraz Sqrt} Write('Odleglosc=',D:0:6); Readln; end. |
Często popełniany błąd polega na próbie wywołania funkcji w następujący sposób:
Sin(X);
Przy takim wywołaniu funkcja, po dokonaniu obliczeń i przyjęciu odpowiedniej wartości, nie dałaby żadnych rezultatów, bo jej wartość nie zostaje nigdzie przekazana. Kompilator nie reaguje na takie wywołania, chyba, że podobnie jak Sin, funkcja znajduje się w module System. Wtedy przy kompilacji pojawia się błąd z komunikatem:
Error 122: Invalid variable reference.
W poleceniu Help/Index środowiska Turbo Pascala znajduje się wykaz wszystkich funkcji i procedur standardowych. Zawarto tam informacje o działaniu podprogramu, jego argumentach, a także przykłady wywołań. Na przykład dla funkcji Sin znajdziemy tam postać nagłówka jej definicji, który wygląda, jak następuje:
function Sin(X:Real):Real;
Nagłówek zawiera bardzo ważne informacje. Słowo kluczowe function informuje nas, że podprogram jest funkcją. W nawiasach podaje się listę argumentów i ich typ - tutaj jest tylko jeden argument typu Real. Po dwukropku podany jest typ wartości zwracanej przez funkcję (to jest wartości, jaką przyjmuje funkcja po dokonaniu obliczeń).
9.4. Instrukcje wywołania procedur
Dotychczas poznaliśmy już kilka instrukcji wywołania procedur standardowych. Były to procedury Readln, Write, Writeln z modułu System oraz procedury Clrscr, Gotoxy, Window z modułu Crt. Spojrzymy teraz na ogólną postać instrukcji wywołania procedury:
nazwa_procedury(arg_1,arg_2, . . . arg_N);
Instrukcja taka składa się z nazwy procedury oraz umieszczonej w nawiasach listy argumentów. Argumenty oddziela się przecinkami. Argumenty mogą mieć postać stałych, zmiennych lub wyrażeń. Jednym z konkretnych przykładów może być instrukcja wywołania znanej nam procedury pisania na ekranie:
Write('X1=',X1,'X2=',X2);
w której zastosowano cztery argumenty: pierwszy i trzeci są stałymi napisami, a drugi i czwarty - zmiennymi, których wartości chcemy wyprowadzić na ekran.
Większość procedur ma określoną liczbę argumentów, każdy z nich określonego typu. Są jednak procedury bezargumentowe (np. Clrscr) oraz takie, które mają zmienną liczbę argumentów i dla każdego z nich akceptują kilka różnych typów danych (np. Write, Readln). W instrukcji wywołania procedury musimy stosować właściwą liczbę argumentów, ich typy i kolejność. W przeciwnym przypadku wystąpi błąd kompilacji, lub rezultaty działania procedury nie będą poprawne.
Wszystkie argumenty można podzielić na dwa rodzaje:
Przekazywane przez wartość. Procedura traktuje takie argumenty jako dane wejściowe, kopiuje je do swojego obszaru pamięci i z tej kopii korzysta, wykonując obliczenia. Procedura w toku obliczeń może zmienić wartość kopii, ale sam argument użyty w wywołaniu nie ulega zmianie. Parametry tej grupy mogą w instrukcji wywołania mieć postać stałej jawnej, stałej definiowanej, zmiennej, lub wyrażenia, przy zachowaniu wymaganej kolejności oraz typów kolejnych argumentów.
Przekazywane przez zmienną. Procedura operuje bezpośrednio na zmiennej, użytej w instrukcji wywołania, więc w toku obliczeń może zmieniać jej wartość. Dlatego ten rodzaj argumentów służy do przekazywania danych wyjściowych, czyli rezultatów działania procedury. Parametry tej grupy w instrukcji wywołania mogą mieć wyłącznie postać nazw zmiennych.
Przykładem procedury, która stosuje wyłącznie argumenty przekazywane przez wartość jest Window. Wywołanie może mieć tutaj postać, w której argumenty są stałymi jawnymi:
Window(1,1,80,25);
albo postać, w której argumenty są zmiennymi o ustalonych wcześniej wartościach:
Window(X1,Y1,X2,Y2);
Przykładem procedury, która stosuje jedynie argumenty przekazywane przez zmienną, jest Readln; Jej argumenty są danymi wyjściowymi, dlatego mogą mieć wyłącznie postać zmiennych typu liczbowego, łańcuchowego lub znakowego, na przykład:
Readln(A,B);
W razie wątpliwości dotyczących rodzaju argumentów danej procedury można zawsze użyć polecenia Help/Index środowiska Turbo Pascala, by skorzystać z opisu interesującej nas procedury. Jednym z elementów opisu jest nagłówek procedury. Na przykład nagłówek procedury Insert, którą poznamy w rozdziale 11, wygląda następująco:
procedure Insert (Source:string; var S:string; Index:Integer);
Z tego nagłówka można uzyskać bardzo ważne informacje. Słowo kluczowe procedure mówi nam, że podprogram jest procedurą. Z listy argumentów w nawiasie dowiadujemy się, że procedura ma trzy argumenty, z tego dwa pierwsze typu string i ostatni typu Integer. Słowo kluczowe var przed drugim argumentem mówi nam, że jest to argument wyjściowy, przekazywany przez zmienną. Dlatego w instrukcji wywołania tej procedury na miejscu drugiego argumentu może wystąpić wyłącznie nazwa zmiennej, na przykład:
Insert('Jan',S,4);
Jak widać, w przykładowej instrukcji wywołania pierwszy i trzeci argument mają postać jawnych stałych - łańcuchowej i liczbowej, natomiast S jest nazwą wcześniej zadeklarowanej i określonej zmiennej typu string. Na miejsce S nie wolno wpisać stałej ani wyrażenia!
9.5. Definiowanie funkcji własnych
Oczywiście, funkcje standardowe oferowane w Turbo Pascalu nie spełniają wszystkich możliwych potrzeb programisty. Dlatego przewidziano możliwość definiowania w programie własnych funkcji. Definicje funkcji własnych umieszcza się przed programem głównym. Każda z nich rozpoczyna się odrębnym słowem kluczowym function. Ogólna postać definicji funkcji jest następująca:
function nazwa(we1:typ_1;we2:typ_2; . . .):typ_wyniku;
var
{deklaracje zmiennych lokalnych, stałych itd.,
jak w sekcji deklaracji programu}
begin
instrukcja_1;
instrukcja_2;
{ - - - }
instrukcja_N;
{przekazanie wyniku:}
nazwa_funkcji:= wynik_obliczeń;
end;
Pierwszy wiersz definicji to nagłówek funkcji. Po słowie kluczowym function występuje nazwa funkcji, która jest wybierana dowolnie; powinna jednak w miarę możności nawiązywać do działania funkcji. Następnie w nawiasach podaje się argumenty (nazwane tutaj we_i) i ich typy. Jeżeli argumenty są tego samego typu, to można utworzyć ich listę, rozdzielając je przecinkami i nazwę typu podać tylko raz. Na końcu nagłówka, po dwukropku, należy wpisać typ wartości obliczanej przez funkcję. Poniżej nagłówka jest miejsce na deklarowanie zmiennych, które funkcja wykorzystuje do pamiętania danych. Są to zmienne lokalne, umieszczane w odrębnym obszarze pamięci, zwanym stosem. Zmienne te istnieją tylko w czasie działania funkcji i są usuwane po zakończeniu jej pracy. Cześć wykonawcza funkcji (zwana ciałem funkcji) jest sekwencją instrukcji, umieszczoną w klamrze begin-end. W odróżnieniu od programu głównego, po słowie end kończącym definicję funkcji nie występuje kropka, lecz średnik.
Wewnątrz ciała funkcji należy wprowadzić specjalną instrukcję przypisania, która do nazwy funkcji przypisuje obliczony wynik. Dopiero po wykonaniu tej instrukcji w odpowiedniej komórce pamięci znajdzie się wynik obliczeń, wykonanych przez funkcję. Jeżeli w ciele funkcji pominiemy wspomnianą instrukcję, to obliczony wynik zostanie utracony, a funkcja po jej wywołaniu przekaże jakąś przypadkową wartość.
Przykład 9.2 przedstawia program z zastosowaniem funkcji własnej, która oblicza wartość silni swojego argumentu. Program ten jest podobny do pokazanego w rozdziale 8, w przykładzie 8.3, lecz różni się tym, że fragment odpowiedzialny za obliczenia został wydzielony i nadano mu postać funkcji. Program główny uprościł się znacznie, ponieważ zawiera jedynie odczyt danej, wywołanie funkcji własnej o nazwie Silnia i druk wyniku.
Przykład 9.2. Program z funkcja własną, która oblicza wartość silni swojego argumentu.
program Ex9_2; uses Crt; var Arg:Byte; S:Longint;
{definicja funkcji) function Silnia(Arg:Byte):Longint; var I:Byte; S:Longint; begin if Arg>12 then S:=0 else begin S:=1; for I:=1 to Arg do S:=S*I; end; Silnia:=S; {instrukcja przekazania wyniku} end;
{program główny} begin Clrscr; Write('Podaj argument silni <13: '); {czytanie argumentu} Readln(Arg); S:=Silnia(Arg); {wywołanie funkcji} if S=0 then Write('Argument za duzy!') {pisanie wyniku} else Write('Silnia liczby ',Arg,' wynosi ',S); Readln; end. |
W kolejnym przykładzie spróbujemy napisać definicję funkcji własnej, której zadaniem jest obliczenie średniej arytmetycznej z trzech liczb całkowitych. Przed przystąpieniem do zadania dobrze jest wyobrazić sobie funkcję jako „czarną skrzynkę”, z pokazanymi w postaci strzałek wejściami, czyli argumentami wejściowymi, oraz wyjściem danych, nadając nazwy wejściom oraz samej funkcji, jak na rysunku 9.1.
X:Integer ——► Y:Integer ——► Z:Integer ——► |
Function Srednia
|
|
│ :Real │ ▼ |
Rys. 9.1. Funkcja własna Srednia jako „czarna skrzynka”
Definicję tej funkcji i cały program, w którym ją wywołano, pokazuje przykład 9.3. Trzeba podkreślić, że zadaniem funkcji jest wyłącznie dokonanie obliczeń i nie należy tego łączyć z innymi zadaniami. Odczyt danych wejściowych odbywa się poza funkcją, w programie głównym, przed jej wywołaniem. Dane zostają zapamiętane w zmiennych programu głównego (t. zw. zmiennych globalnych) A, B, C. Po wywołaniu funkcji, aktualne wartości A, B, C są przepisywane do argumentów X, Y, Z funkcji Srednia, jako dane wejściowe do obliczeń. Pisania rezultatu również nie należy umieszczać wewnątrz funkcji. Powinno to odbyć się w programie głównym po wywołaniu funkcji Srednia, Zwracana przez nią wartość zostaje zapisana w zmiennej globalnej Wynik i następnie wyprowadzona na ekran jako ostateczny rezultat działania programu.
Przykład 9.3. Program z funkcją własną, obliczającą średnią z trzech liczb
program Ex9_3; uses Crt; var A,B,C:Integer; Wynik:Real;
{definicja funkcji} function Srednia(X,Y,Z:Integer):Real; var S:Real; begin S:=(X+Y+Z)/3; Srednia:=S; {przekazanie wyniku} end;
{program główny} begin Clrscr; Write('Podaj trzy liczby: '); {odczyt danych} Readln(A,B,C); Wynik:=Srednia(A,B.C); {wywołanie funkcji} Write('Wynik: ',Wynik); {wyprowadzenie wyniku} Readln; end.
|
9.6. Definiowanie procedur własnych
Poniżej pokazano ogólną postać definicji procedury.
procedure nazwa_procedury(we1:typ1; . . . var wy1:typ1; . . .);
var
{deklaracje zmiennych lokalnych, stałych itd.,
jak w sekcji deklaracji programu}
begin
instrukcja_1;
instrukcja_2;
{ - - - }
instrukcja_N;
end;
Procedura może zawsze zastąpić funkcję, a funkcja - procedurę. W przykładzie 9.4 zobaczymy, jak można zdefiniować i wykorzystać w programie procedurę, równoważną pod względem działania funkcji Srednia z przykładu 9.3. Przedtem wyobrazimy sobie tę procedurę jako „czarną skrzynkę”, pokazując wejścia i wyjścia danych, czyli argumenty we/wy, oraz ustalając ich nazwy i typy (rysunek 9.2). Procedura będzie miała trzy wejścia A, B, C, ale tylko jedno wyjście, które nazwiemy S. Typem wyjścia jest Real, bo użycie operatora dzielenia `/' powoduje, że wartość wyrażenia obliczającego średnią musi być właśnie tego typu.
A: Integer ——► B: Integer ——► C: Integer ——► |
procedure Srednia
|
——► var S: Real |
Rys. 9.2. Procedura własna Srednia jako „czarna skrzynka”
Przykład 9.4. Program z procedurą własną obliczającą średnią z trzech liczb
program Ex9_4; uses Crt; var A,B,C:Integer; Wynik:Real;
{definicja procedury} procedure Srednia(X,Y,Z:Integer; var S:Real); begin S:=(X+Y+Z)/3; end;
{program główny} begin Clrscr; Write('Podaj trzy liczby: '); {odczyt danych} Readln(A,B,C); Srednia(A,B.C,Wynik); {wywołanie procedury} Write('Wynik: ',Wynik); {wyprowadzenie wyniku} Readln; end.
|
Porównajmy postać funkcji Srednia z postacią procedury o tej samej nazwie, a także sposoby wywołania obu tych podprogramów. Zauważamy, że:
W nagłówku funkcji brak argumentu wyjściowego; zamiast tego po nawiasie i dwukropku jest typ wyniku, a w ciele funkcji znajduje się instrukcja, przypisująca obliczony wynik do nazwy funkcji.
W nagłówku procedury w nawiasie pojawia się argument wyjściowy S, który przejmuje rolę zmiennej lokalnej S w definicji funkcji. Do niego po obliczeniu średniej przesłany będzie wynik, więc zmienna lokalna nie jest potrzebna.
W ciele definicji procedury nie ma instrukcji przypisującej nazwie wynik obliczeń.
Funkcję wywołuje się w programie głównym po prawej stronie instrukcji przypisania. Jej wartość przypisujemy do zmiennej globalnej Wynik, by potem wyprowadzić ją na ekran.
Procedurę wywołuje się w postaci instrukcji wywołania, która składa się z nazwy funkcji oraz umieszczonych w nawiasach jej argumentów. Dane wejściowe przekazuje się tak jak do funkcji, przez aktualne wartości zmiennych globalnych A, B, C. Natomiast czwarty argument wywołania Wynik odpowiada czwartemu argumentowi S w definicji procedury. W tej definicji przed S wpisano słowo var. Dlatego ten argument będzie traktowany jako wyjście procedury. Wszelkie operacje na S są w rzeczywistości operacjami na zmiennej globalnej Wynik, której użyto na czwartej pozycji w instrukcji wywołania.
9.7. Przekazywanie danych pomiędzy programem głównym a podprogramami
Zanim opiszemy transfer danych pomiędzy programem głównym a procedurami i funkcjami, musimy zapoznać się ze sposobem, w jaki Turbo Pascal korzysta z dostępnych mu zasobów pamięci. Jak pokazano na rysunku 9.3, pamięć ta dzieli się na kilka obszarów. W pierwszym mieści się sam program zapisany w języku wewnętrznym komputera i dołączone do niego moduły programowe. Następny obszar jest przeznaczony do przechowania zmiennych globalnych deklarowanych w programie. Obszar ten ma stały rozmiar 64K, a umieszczone w nim zmienne istnieją tak długo, jak długo wykonywany jest program. Dalej znajduje się tak zwany stos (ang. stack), na którym, jeden po drugim, są lokowane argumenty i zmienne lokalne procedur i funkcji. Obszary dla zmiennych lokalnych i argumentów są zarezerwowane tylko na czas działania aktualnie wykonywanej procedury lub funkcji. Po zakończeniu działania podprogramu są one natychmiast zwalniane, więc tracimy zapamiętane w nich wartości. Dlatego funkcje i procedury muszą przed zakończeniem swojego działania przekazać wyniki do programu głównego, gdzie te wyniki zostaną odpowiednio wykorzystane.
Pokażemy dokładnie przebieg transferu danych pomiędzy programem głównym a procedurami, korzystając z przykładu 9.5. Pokazano tam program, przeznaczony do znajdowania rozwiązań równania kwadratowego o postaci: ax2+bx+c=0. Jest to program w pełni strukturalny, w którym kolejno wykonywane działania cząstkowe: odczyt danych, obliczenia, wyprowadzenie
STERTA ZMIENNE DYNAMICZNE rozmiar <= 655 kilobajtów |
STOS ZMIENNE LOKALNE, ARGUMENTY PODPROGRAMÓW Rozmiar domyślny: 16 kilobajtów |
OBSZAR ZMIENNYCH GLOBALNYCH ZMIENNE GLOBALNE PROGRAMU Rozmiar: 64 kilobajty |
OBSZAR KODU WYNIKOWEGO DEKLAROWANE MODUŁY+MODUŁ SYSTEM + SKOMPILOWANY PROGRAM .EXE Rozmiar: co najwyżej 64 K dla każdego z modułów i dla programu |
Rys. 9.3. Organizacja zasobów pamięci operacyjnej Turbo Pascala
rezultatów, zostały powierzone trzem odrębnym procedurom własnym.. Pierwsza procedura Dane ma tylko trzy argumenty wyjściowe, za których pośrednictwem przekazuje do programu głównego odczytane z klawiatury wartości A, B, C. Trzecia procedura Wyniki ma tylko trzy argumenty wejściowe. Wartość argumentu Kw decyduje o tym, czy drukować wartości X1, Y1, czy też wyprowadzić komunikat o braku pierwiastków. Najbardziej złożona jest druga procedura Rowkwad, która oblicza rozwiązania X1, X2. Rysunek 9.4 pokazuje ją jako „czarną skrzynkę”. Wejściami są parametry równania Ap, Bp, Cp. Argumentami wyjściowymi są X1p, X2p. Procedura ma też dodatkowe wyjście kontrolne Kp. Jeżeli obliczenia mogą być wykonane, to Kp=0. W sytuacjach specjalnych, gdy Ap=0 lub Delta<0, równanie nie ma rozwiązań i wyjście kontrolne Kp=1. Wartość Kp, przekazywana jako zmienna K do programu głównego, wykorzystana jest potem jako jeden z argumentów wejściowych procedury Wyniki, która w zależności od wartości K albo wyprowadza X1, X2, albo informuje o braku rozwiązań.
Przykład 9.5. Program strukturalny znajdujący rozwiązania równania kwadratowego
program Ex9_5; {Znajduje rozwiązania równania kwadratowego.} uses Crt; var A,B,C,X1,X2:Real; K:Byte;
procedure Dane(var Ad,Bd,Cd:Real); begin Write('Podaj A,B,C: '); Readln(Ad,Bd,Cd); end;
procedure Rowkwad(Ap,Bp,Cp:Real;var X1p,X2p:Real;var Kp:Byte); var Delta:Real; begin Delta:=Bp*Bp-4*Ap*Cp; if (Delta<0)or(Ap=0) then Kp:=1 else begin Kp:=0; X1p:=(-Bp+Sqrt(Delta))/(2*Ap); X2p:=(-Bp-Sqrt(Delta))/(2*Ap); end; end;
procedure Wyniki(X1w,X2w:Real;Kw:Byte); begin if Kw=0 then begin Writeln('X1=',X1w); Writeln('X2=',X2w); end else Writeln('Brak pierwiastkow.'); end;
{program główny} begin Clrscr; Dane(A,B,C); Rowkwad(A,B,C,X1,X2,K); Wyniki(X1,X2,K); Readln; end.
|
Ap:Real ——► Bp:Real ——► Cp:Real ——► |
procedure Rowkwad
|
——► var X1p: Real ——► var X2p: Real ——► var Kp: Byte |
Rys. 9.4. Procedura własna Rowkwad jako „czarna skrzynka”
Rysunek 9.5 pokazuje zawartość zmiennych globalnych, lokalnych i argumentów po wywołaniu kolejnych procedur.
Po wywołaniu procedury Dane(A,B.C), odczytane z klawiatury wartości 2.0, 13,0, 5.0 zostają przekazane do zmiennych globalnych A, B, C. Dzieje się tak dlatego, że argumenty Ad, Bd, Cd poprzedzone były słowem var i traktowane jako wyjściowe. W takim przypadku procedura po prostu zastępuje nazwy argumentów występujące w definicji tymi nazwami, których użyto w instrukcji wywołania. Nazwa Ap zostaje zastąpiona przez A, Bp przez B, a Cp przez C. Jak widać, procedura operuje bezpośrednio na zmiennych programu głównego. Przy wywołaniu omawianej procedury Dane obszar stosu pozostaje pusty, ponieważ nie ma ona argumentów wejściowych ani zmiennych lokalnych.
Po wywołaniu procedury Rowkwad(A,B,C,X1,X2,K), tworzą się cztery obszary na stosie - trzy dla argumentów wejściowych Ap, Bp, Cp i jeden dla zmiennej lokalnej Delta. Potem następuje przekopiowanie danych, pamiętanych w zmiennych globalnych A, B, C odpowiednio do obszarów Ap, Bp, Cp na stosie i na tych ostatnich procedura będzie wykonywała obliczenia. Najpierw zostaje obliczona i zapamiętana wartość Delta=129.0. Następnie są obliczane rozwiązania równania kwadratowego, oznaczone w definicji procedury jako X1p, X2p, Kp. Ponieważ jednak były to argumenty wyjściowe, poprzedzone słowem var, procedura operuje w rzeczywistości na zmiennych globalnych X1, X2, K użytych w instrukcji wywołania, przekazując do nich obliczone wartości, to jest odpowiednio: 0.411, -6.089, 0.
Dane(A,B,C); |
|||||||||||
|
A |
|
B |
|
C |
|
X1 |
|
X2 |
|
K |
Zmienne globalne: |
2.0 |
|
13.0 |
|
5.0 |
|
? |
|
? |
|
? |
Zmienne lokalne i argumenty: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Rowkwad(A,B,C,X1,X2,K); |
|||||||||||
|
A |
|
B |
|
C |
|
X1 |
|
X2 |
|
K |
Zmienne globalne: |
2.0 |
|
13.0 |
|
5.0 |
|
- 0.411 |
|
- 6.089 |
|
0 |
Zmienne lokalne i argumenty: |
Ap ↓ |
|
Bp ↓ |
|
Cp ↓ |
|
|
|
Delta |
|
|
|
2.0 |
|
13.0 |
|
5.0 |
|
|
|
129.0 |
|
|
Wyniki(X1,X2,K); |
|||||||||||
|
A |
|
B |
|
C |
|
X1 |
|
X2 |
|
K |
Zmienne globalne: |
2.0 |
|
13.0 |
|
5.0 |
|
- 0.411 |
|
- 6.089 |
|
0 |
Zmienne lokalne i argumenty: |
|
|
|
|
|
|
X1w ↓ |
|
X2w ↓ |
|
Kw ↓ |
|
|
|
|
|
|
|
-0.411 |
|
-6.089 |
|
0 |
Rys. 9.5. Transfer danych między programem a procedurami w przykładzie 9.4
Wywołanie trzeciej i ostatniej procedury własnej spowoduje utworzenie trzech obszarów na stercie: X1w, X2w, Kw. Do nich są kopiowane użyte w instrukcji wywołania wartości globalnych zmiennych X1, X2, K. Z tych przekopiowanych wartości skorzysta procedura, by wyprowadzić na ekran wyniki obliczeń programu.
9.9. Funkcja jako podprogram uniwersalny
W literaturze często się pisze, że funkcja może zwrócić tylko jedną wartość typu prostego (liczbowego, znakowego) lub typu string. Pokażemy, że taka informacja nie jest całkowicie prawdziwa. Dotyczy ona jedynie wartości zwracanych przez funkcję jako całość. Na przykład standardowa funkcja Sin o nagłówku
function Sin(X:Real):Real;:
wywołana, jak następuje:
Y:=Sin(X);
zwraca tylko jedną wartość. Jest to obliczona przez nią wartość sinusa kąta X, przekazywana przez przypisanie do zmiennej Y. Nie ma jednak powodu, by funkcja nie mogła zwracać wyników również przez argumenty wyjściowe, poprzedzone słowem var, podobnie jak to ma miejsce w przypadku procedury. Na przykład funkcja własna o nagłówku:
function Rowkwad(A,B,C:Real;var X1,X2:Real):Byte;
wywołana, jak poniżej:
K:=Rowkwad(A,B,C,X1,X2);
zwróci trzy wartości - dwie wartości typu Real przez argumenty X1, X2 oraz jedną wartość typu Byte, przekazaną przez przypisanie do zmiennej K.
Pokazaliśmy, że funkcja jest podprogramem uniwersalnym, który może zastąpić każdą procedurę. W języku C++ nie ma procedur; używa się w nim wyłącznie funkcji.
W przykładzie 9.6 pokazano program znajdujący rozwiązania równania kwadratowego, w którym wszystkie procedury z przykładu 9.5 zastąpiono odpowiednimi funkcjami.
Przykład 9.6. Program z przykładu 9.5, w którym procedury zastąpiono funkcjami
program Ex9_6; {Znajduje rozwiązania równania kwadratowego.} uses Crt; var A,B,C,X1,X2:Real; K:Byte; function Dane(var Ad,Bd,Cd:Real):Byte; begin Write('Podaj A,B,C: '); Readln(Ad,Bd,Cd); Dane:=0; end; function Rowkwad(Ap,Bp,Cp:Real; var X1p,X2p:Real):Byte; var Delta:Real; begin Delta:=Bp*Bp-4*Ap*Cp; if (Delta<0)or(A=0) then Rowkwad:=1 else begin Rowkwad:=0; X1p:=(-Bp+Sqrt(Delta))/(2*Ap); X2p:=(-Bp-Sqrt(Delta))/(2*Ap); end; end; |
Przykład 9.5, c.d.
function Wyniki(X1w,X2w:Real;Kw:Byte):Byte; begin if Kw=0 then begin Writeln('X1=',X1w); Writeln('X2=',X2w); end else Writeln('Brak pierwiastkow.'); Wyniki:=0; end; begin Clrscr; Dane(A,B,C); K:=Rowkwad(A,B,C,X1,X2); Wyniki(X1,X2,K); Readln; end. |
9.10. Typowe błędy przy definiowaniu i wywoływaniu podprogramów
W tym miejscu chcemy zwrócić uwagę na błędy, popełniane przez początkujących programistów przy pisaniu definicji funkcji lub procedur. Program z przykładu 9.7 został przedstawiony w postaci napisanej prawidłowo, a następnie w czterech niepoprawnych wersjach, które demonstrują najczęściej obserwowane rodzaje błędów. Nie chodzi tutaj o błędy kompilacji, ani błędy logiczne. Wszystkie pokazane poniżej wersje błędnych programów kompilują się i działają prawidłowo. Jednak sposób, w jaki w nich napisano lub wywołano funkcję własną, łamie podstawowe zasady poprawnego programowania i dlatego nie można go zaakceptować.
Przykład 9.7. Poprawny program do obliczania sumy N początkowych liczb naturalnych
Program Ex9_7; {Znajduje sumę N początkowych liczb naturalnych.} uses Crt; var N,Suma:Word; K:Shortint; function Sumanat(N:Word; var Suma:Word):Shortint; var I:Word; S:Longint; K:Shortint; begin S:=0; K:=0; for I:=1 to N do begin S:=S+I; if S>65535 then begin K:=-1; break; end; end; Suma:=S; Sumanat:=K; end; begin Clrscr; Write('Podaj N<362: '); Readln(N); K:=Sumanat(N,Suma); if K=0 then Write('Suma=',Suma) else Write('Za duze N.'); Readln; end. |
Na początek omówimy działanie poprawnej wersji programu. Funkcja Sumanat zwraca dwa wyniki. Jednym z nich jest suma kolejnych liczb naturalnych od 1 do N, gdzie N jest argumentem wejściowym funkcji. Obliczenia są wykonywane iteracyjnie przez instrukcję for. Wewnątrz tej instrukcji, po każdym dodaniu kolejnej wartości I do sumy cząstkowej S, odbywa się kontrola bieżącej wartości S. Po przekroczeniu zakresu typu Word, wynik zwracany przez funkcję byłby błędny. Dlatego w takim przypadku instrukcja break przerywa obliczenia i zmienna kontrolna K przyjmuje wartość -1. Jeżeli natomiast do końca obliczeń wartość S nie przekroczyła dopuszczalnej wartości, to obliczona suma zostaje przekazana do argumentu wyjściowego Suma, a zmienna kontrolna K zachowuje nadaną jej na początku wartość 0. Przed zakończeniem działania funkcji, zmienna K zostaje przypisana do nazwy funkcji. Dzięki temu funkcja przekazuje jej wartość do programu głównego, jako drugi z obliczanych przez siebie wyników.
Przykład 9.8 pokazuje często spotykany błąd, polegający na użyciu w ciele podprogramu zmiennych globalnych. W przykładzie użyto w ten sposób zmiennych I oraz S, mimo że obie te zmienne potrzebne są jedynie w czasie działania funkcji - pierwsza z nich jest zmienną sterującą pętli for, a druga służy do przechowania sumy cząstkowej w czasie obliczeń iteracyjnych. Tego rodzaju postępowanie pozbawia podprogram jednej z najważniejszych jego zalet, to jest możliwości natychmiastowego wykorzystania w dowolnym innym programie. Inną konsekwencją tego błędu jest trudniejsza analiza programu i znacznie bardziej kłopotliwa identyfikacja błędów logicznych w przypadku jego niewłaściwego działania.
W ciele funkcji lub procedury nie wolno używać zmiennych globalnych. Wszystkie użyte tam zmienne muszą być deklarowane jako zmienne lokalne lub argumenty.
Przykład 9.8. Niepoprawny program - funkcja stosuje zmienne globalne
program Ex9_8; uses Crt; var I,N,Suma:Word; {Zmienna I powinna być zmienną lokalną!} S:Longint; {Zmienna S powinna być zmienną lokalną!} K:Shortint;
function Sumanat(N:Word; var Suma:Word):Shortint; var K:Shortint; begin S:=0; K:=0; {Zmienne S, I są globalne - to błąd!} for I:=1 to N do begin S:=S+I; if S>65535 then begin K:=-1; break; end; end; Suma:=S; Sumanat:=K; end; begin Clrscr; Write('Podaj N<362: '); Readln(N); K:=Sumanat(N,Suma); if K=0 then Write('Suma=',Suma) else Write('Za duze N.'); Readln; end. |
Program z przykładu 9.9 pokazuje inny często popełniany błąd, polegający na myleniu zmiennych lokalnych z argumentami podprogramu. Mówiliśmy wcześniej, że przed przystąpieniem do pisania definicji podprogramu należy zidentyfikować dane wejściowe i wyjściowe. Argumenty wejściowe służą do przekazania z programu głównego do podprogramu danych, potrzebnych do przeprowadzenia obliczeń. Argumenty wyjściowe przekazują do programu głównego wyniki, uzyskane w toku działania podprogramu. W rozważanym przypadku jedyną daną wejściową jest liczba sumowanych liczb N, a jedyną przekazywaną przez argument daną wyjściową - obliczona suma liczb Suma. (Wartość kontrolna K zwracana jest jako wartość funkcji.). W demonstrowanym programie popełniono błąd, polegający na potraktowaniu zmiennych lokalnych I oraz S jako argumentów wejściowych przez wpisanie ich do nawiasu po nazwie funkcji. Takie postępowanie nie spowoduje błędnego działania programu, ale utrudni jego analizę. Ponadto w instrukcji wywołania będzie trzeba wpisać dodatkowo na pozycjach tych niepotrzebnych argumentów dwie przypadkowe wartości (lub niepotrzebne zmienne globalne). W rozpatrywanym przykładzie wpisano wartości 0,0.
Nie wolno wpisywać zmiennych lokalnych do listy argumentów procedury lub funkcji. Argumenty reprezentują wyłącznie dane we/wy. Wszelkie inne zmienne należy definiować jako lokalne.
Przykład 9.9. Niepoprawny program - zbędne argumenty zamiast zmiennych lokalnych
program Ex9_9; uses Crt; var N,Suma:Word; K:Shortint;
function Sumanat(N,I:Word; S:Longint; var Suma:Word):Shortint; var K:Shortint; {I oraz S powinny być zmiennymi lokalnymi!} begin S:=0; K:=0; for I:=1 to N do begin S:=S+I; if S>65535 then begin K:=-1; break; end; end; Suma:=S; Sumanat:=K; end;
begin Clrscr; Write('Podaj N<362: '); Readln(N); K:=Sumanat(N,0,0,Suma); if K=0 then Write('Suma=',Suma) else Write('Za duze N.'); Readln; end.
|
W przykładzie 9.10 pokazano jeszcze inny często spotykany rodzaj błędu. W ciele funkcji, której zadaniem jest przeprowadzenie obliczeń z wykorzystaniem danej przekazanych przez argument wejściowy N, znalazł się dialog z użytkownikiem, który jest pytany o wartość N. Złamano tutaj dwie zasady: po pierwsze - jeśli dopiero w ciele funkcji określamy wartość N, to argument N jest niepotrzebny i powinien być zastąpiony zmienna lokalną. Po drugie - w jednej funkcji nie należy łączyć dwóch różnych zadań, to jest wprowadzania danych i wykonywania obliczeń. Zauważmy, że żadna z arytmetycznych funkcji standardowych Turbo Pascala, takich jak Sin, Sqr, Sqrt, Ln i inne, nie pyta użytkownika o dane wejściowe, lecz otrzymuje je przez argument wejściowy.
Nie wolno w jednym podprogramie łączyć kilku różnych działań. W podprogramach służących do obliczeń nie należy umieszczać instrukcji czytania danych z klawiatury. Dane powinny być przekazywane przez argumenty wejściowe.
Przykład 9.10. Niepoprawny program - wprowadzanie danych w ciele funkcji obliczającej
program Ex9_10; uses Crt; var N,Suma:Word; K:Shortint; function Sumanat(N:Word; var Suma:Word):Shortint; var I:Word; S:Longint; K:Shortint; begin Write('Podaj N<362: '); Readln(N); {N jest argumentem, więc jego wartość powinna być ustalana poza funkcją!} S:=0; K:=0; for I:=1 to N do begin S:=S+I; if S>65535 then begin K:=-1; break; end; end; Suma:=S; Sumanat:=K; end;
begin Clrscr; K:=Sumanat(N,Suma); if K=0 then Write('Suma=',Suma) else Write('Za duze N.'); Readln; end.
|
Błąd, którego ilustracją jest przykład 9.11, również polega na połączeniu w jednym programie dwóch różnych działań: obliczeń oraz wyprowadzenia wyniku na ekran. Ponadto występuje tutaj jeszcze jedna nieprawidłowość: skoro rezultaty są wyprowadzone na ekran w ciele funkcji, to argument wyjściowy Suma jest zbyteczny i powinien być zastąpiony przez zmienną lokalną, a instrukcja przypisująca K do nazwy funkcji jest do pominięcia. Wtedy w programie głównym niepotrzebna będzie zmienna Suma, podobnie jak zmienna K, której wartość nie jest w ogóle wykorzystywana.
W jednym podprogramie nie należy łączyć obliczeń z wyprowadzaniem na ekran ich wyników. Rezultaty obliczeń należy przekazywać przez argument wyjściowy. Druk wyników trzeba powierzyć odrębnej procedurze.
Przykład 9.11. Niepoprawny program - wyprowadzanie wyników w ciele funkcji obliczającej
program Ex9_11; uses Crt; var N,Suma:Word; K:Shortint;
function Sumanat(N:Word; var Suma:Word):Shortint; var I:Word; S:Longint; K:Shortint; begin Write('Podaj N<362: '); Readln(N); S:=0; K:=0; for I:=1 to N do begin S:=S+I; if S>65535 then begin K:=-1; break; end; end; Suma:=S; Sumanat:=K; if K=0 then Write('Suma=',Suma) {Argument Suma oraz zwracane K } else Write('Za duze N.'); {Argument Suma oraz zwracane K powinny być użyte poza funkcją!} end;
begin Clrscr; K:=Sumanat(N,Suma); Readln; end.
|
65