W najprostszej postaci procedury i funkcje wykorzystuje się je w celu zmniejszenia wielkości kodu programu. Załóżmy, że w kilku miejscach programu chcialibyśmy wykonać pewien ciąg instrukcji. Zamiast powielać ten sam ciąg instrukcji po wielekroć możemy w programie zadeklarować procedurę lub funkcję, której ciało zawiara ten ciąg. Następnie, zamiast tego ciągu w programie umieścić należy wywołania tak zadeklarowanej procedury czy fumkcji.
Zacznijmy od składni procedur.<dekl_Procedury> ::=
procedure <Ident> [( <param_Formalne>) ];
<dekl_Stałych>
<dekl_Typów>
<dekl_Zmiennych>
<dekl_Procedur_lub_Funkcji>
begin
<Ciąg_Instrukcji>
end;
Powyższe należy traktować jak przepis definiowania poprawnie
zbudowanych procedur. I tak, deklaracja procedury ma postać
podobną do programu pascalowego, z tym że zamiest słowa kluczowego
W istocie procedury i funkcje mogą spełniać rolę mechanizmu ułatwiającego konstrukcję algorytmów. Częstokroć w problemie, który mamy zalgorytmizować, daje się wyodrębnić prostsze podproblemy, których rozwiązanie pozwala podać rozwiązanie całego problemu.
Na przykład rozważmy takie zadanie:
Napisz program, który wczytuje z wejścia standardowego dwie liczby,
powiedzmy
Jeśli założymy, że mamy daną metodę drukowania jednego wiersza, to
rozwiązywać powyższe zadanie jest nietrudno: wystarczy
Powyższe rozwiązanie ma charakter hipotetyczny. Zakładając, że znajdziemy rozwiązanie prostszego problemu - drukowania jednego wiersza - znaleźliśmy rozwiązanie głównego problemu.program druk_prost; var i, w, k : integer; procedure druk_wiersza; begin (* druk jednego wiersza *) end { druk_wiersza }; begin readln(w,k); for i := 1 to w do druk_wiersza end.
Możemy skoncentrować się teraz na podbroblemie. Po to, by wydrukować
wiersz długości
gdziefor j := 1 to k do write('@'); writeln
Należy zwrócić uwagę, że w programie trzeba było dodać również stosowną deklarację globalnej zmiennejprogram druk_prost; var i, j, w, k : integer; procedure druk_wiersza; begin for j := 1 to k do write('@'); writeln end { druk_wiersza }; begin readln(w,k); for i := 1 to w do druk_wiersza end.
program druk_prost; var i, j, w, k : integer; begin readln(w,k); for i := 1 to w do begin for j := 1 to k do write('@'); writeln end end.
Naszkicowany powyżej sposób postępowanie można z powodzeniem wykorzystywać w produkcji większych systemów oprogramowania. Przy większych projektach jest znaczną niedogodnością konieczność ograniczania inwencji nazewniczej programisty, by uniknąć niezręcznych sytuacji, gdy ta sama zmienna wykorzystywana jest w kilku różnych rolach. W powyższym przykładzie moglibyśmy przecież, myśląc o rozwiązaniu podproblemu, zaproponować jako rozwiązanie
gdziefor i := 1 to k do write('@'); writeln
równoważny byłby następującemu programowi bez procedur:program druk_prost; var i, w, k : integer; procedure druk_wiersza; begin for i := 1 to k do write('@'); writeln end { druk_wiersza }; begin readln(w,k); for i := 1 to w do druk_wiersza end.
Jak widać w pętli wewnętrznej wartość zmiennej regulującej liczbę wykonań pętli zewnętrznej ulega zmianie. W efekcie, jeśli wczytamy wartościprogram druk_prost; var i, w, k : integer; begin readln(w,k); for i := 1 to w do begin for i := 1 to k do write('@'); writeln end end.
By zapobiec tego typu sytuacjom, jak również po to by uwolnic
programistów od kłopotliwego pamiętania o tym jakie nazwy zostały
już użyte, wprowadzono mechanizm deklaracji lokalnych.
W odniesieniu do naszego przykładu, myśląc o podprogramie druku
wiersza moglibyśmy, tylko na potrzebywykonania ciała procedury,
wprowadzić zmienną lokalną do kontroli liczby wykonań wydruku znaku
program druk_prost; var i, w, k : integer; procedure druk_wiersza; var i : integer; begin fori := 1 to k do write('@'); writeln end { druk_wiersza }; begin readln(w,k); for i := 1 to w do druk_wiersza end.
UWAGA! Powyżej, celem wyjaśnienia idei, wprowadzono notację rodem jeszcze z języka Algol60, pozwalającą wprowadzać deklaracje lokalne w każdym bloku. Nie jest to konwencja dopuszczalna w języku Pascal.program druk_prost; var i, w, k : integer; begin readln(w,k); for i := 1 to w do begin var i_z_druk_wiersza : integer; fori_z_druk_wiersza := 1 to k do write('@'); writeln end end.
Jak na powyższym przykładzie widać, idea procedury daje się łatwo wyjaśnić poprzez odwołanie się do pojęcia zestąpienia tekstu wywołania procedury innym tekstem - zmodyfikowanym lub nie tekstem ciała tej procedury. Dodatkowym mechanizmem zwiększającym praktyczną przydatność procedur jest uzależnienia spobu działania procedury w momencie każdego wywołania od wartości pewnych parametrów.
Wracając do przykładu - poddano już krytyce wykorzystanie zmiennej
globalnej do realizacji pewnych zadań lokalnych - w przykładzie
chodziło o odliczanie liczby wydruków znaku
z treści wywołania proceduryprogram druk_prost; var i, w, k : integer; procedure druk_wiersza; var i : integer; begin for i := 1 to k do write('@'); writeln end { druk_wiersza }; begin readln(w,k); for i := 1 to w do druk_wiersza end.
By uczytelnić programowanie i umożliwić takie uzależnienie działania
procedur od wartości pewnych wyrażeń, wprowadzono mechanizm
parametrów wołanych przez wartość. W naszym przypadku
należałoby zadeklarować procedurę
program druk_prost; var i, w, k : integer; procedure druk_wiersza ( ile : integer ) ; var i : integer; begin for i := 1 toile do write('@'); writeln end { druk_wiersza }; begin readln(w,k); for i := 1 to w do druk_wiersza(k) end.
Postępowanie taki może okazać się przydatne, jeśli na przykład zechcemy zmodyfikować zadanie programistyczny i myśleć o drukowaniu bardziej wymyślnych kształtów. Na przykład program
drukował będzie trójkąt. Dlaprogram druk_prost; var i, n : integer; procedure druk_wiersza ( ile : integer ) ; var i : integer; begin for i := 1 toile do write('@'); writeln end { druk_wiersza }; begin readln(n); for i := 1 to n do druk_wiersza(i) end.
@ @@ @@@ @@@@ @@@@@ @@@@@@ @@@@@@@
Znaczenie procedury z deklaracją lokalną możnaby wyjaśnić następująco. Program z wywołaniem takiej procedury równoważny jest programowi w którym w miejsce każdego wywołania wpisano zmodyfikowane ciało procedury. Modyfikacja ta sprowadzać się powinna do zastąpienia każdego wystąpienia parametru formalnego wołanego przez wartość jakąś nową nazwą, która nie pojawiła się jeszcze w programie. w naszym przykładzie możnaby to uzyskać na przykład następująco.
Jak widać różnica w traktowaniu zmiennych lokalnych i parametrów wołanych przez wartość sprowadza się do przekazania wartości parametrowi w momencie wywołania.program druk_prost; var i, w, k : integer; begin readln(w,k); for i := 1 to w do begin var i_z_druk_wiersza : integer; var ile_z_druk_wiersza : integer; ile_z_druk_wiersza := k; for i_z_druk_wiersza := 1 toile_z_druk_wiersza do write('@'); writeln end end.
UWAGA! Jak poprzednio, celem przekazania idei, użyto notacji deklaracji lokalnej w bloku, która nie jest dopuszczalna w języku Pascal.
Parametryzacja procedur opisana powyżej ma tę istotną wadę, że nie pozwala na eksport wyników obliczeń przeprowadzonych w ciele procedury na zewnątrz, poza jej ciało. Można to, oczywiście, osiągnąć korzystając ze zmiennych globalnych. Utrudniać to będzie jednak analizowanie tekstów długich programów, bo wymagałoby od każdego z programistów pamiętania tego, która procedura może zmieniać jakie zmienne globalne.
By unikąć tego dylematu w Pascalu można wołać parametry przez
nazwę lub lokację. Idea jest taka sama jak w
przypadku procedur wejścia/wyjścia. Przy drukowaniu parametry
wołane są przez wartość:
W sposób naturalny procedury z parametrami wołanymi przez zmienną stosuje się do obróbki złożonych struktur danych: tablic, rekordów, a zwłaszcza struktur dynamicznych (listy, drzewa, ...). Idea jest zawsze taka sama - chodzi o zmianę stanu jakieś zmiennej.
W obu procedurach wykorzystano stałą globalnąprogram tablice; const n = 5; type tab = array [1..n] of real; var t1, t2, t3 : tab; procedure generuj ( var t : tab ) ; var i : integer; begin randomize; for i := 1 ton dot [i] := random end { generuj }; procedure drukuj (t : tab ) ; var i : integer; begin for i := 1 ton do write(t [i]:8:5) end { drukuj }; begin generuj(t1); generuj(t2); generuj(t3); writeln('tablica 1'); drukuj(t1); writeln('tablica 2'); drukuj(t2); writeln('tablica 3'); drukuj(t3); end.
Wykonanie powyższego programu dać może następujące rezultaty:
sh-2.05b$ ./tablice tablica 1 0.41835 0.89078 0.54621 0.61766 0.71612 tablica 2 0.32527 0.35698 0.49459 0.63709 0.50140 tablica 3 0.30091 0.66060 0.05772 0.36933 0.44402 sh-2.05b$
sh-2.05b$ fpc tablice.pas Free Pascal Compiler version 1.9.4 [2004/05/30] for i386 Copyright (c) 1993-2004 by Florian Klaempfl Target OS: Linux for i386 Compiling tablice.pas tablice.pas(20,4) Warning: Variable "t1" does not seem to be initialized tablice.pas(21,4) Warning: Variable "t2" does not seem to be initialized tablice.pas(22,4) Warning: Variable "t3" does not seem to be initialized Linking tablice 26 Lines compiled, 0.1 sec sh-2.05b$ ./tablice tablica 1 0.00000 0.00000 0.00000 0.00000 0.00000 tablica 2 0.00000 0.00000 0.00000 0.00000 0.00000 tablica 3 0.00000 0.00000 0.00000 0.00000 0.00000 sh-2.05b$
UWAGA! W Pascalu parametry wołane przez zmienną można w pewnych sytuacjach zastąpić funkcjami zwracającymi wartości odpowiedniego typu. W zasadzie ogranicza się to jednak tylko do jednej wartości i to wartości typu prostego. To ostatnie zależne bywa od implementacji.
Powyższy przykład demonstruje jednocześnie oba sposoby wołania parametrów. W ogólnym przypadku procedura może mieć wiele parametrów i/lub zmiennych lokalnych. Wszystkie one obsługiwane są według przedstawionych tu zasad.program tablice; const n = 5; type tab = array [1..n] of real; var t1, t2, t3 : tab; begin begin var i_z_generuj : integer; randomize; for i_z_generuj := 1 to n dot1 [i] := random end { generuj }; beginvar i_z_generuj : integer; randomize; for i_z_generuj := 1 to n dot2 [i] := random end { generuj }; beginvar i_z_generuj : integer; randomize; for i_z_generuj := 1 to n dot3 [i] := random end { generuj }; writeln('tablica 1'); beginvar i_z_drukuj : integer; var t_z_drukuj : tab; t_z_drukuj := t1; for i_z_generuj := 1 to n do write(t_z_drukuj [i]:8:5); writeln end { drukuj }; writeln('tablica 2'); beginvar i_z_drukuj : integer; var t_z_drukuj : tab; t_z_drukuj := t2; for i_z_generuj := 1 to n do write(t_z_drukuj [i]:8:5); writeln end { drukuj }; writeln('tablica 3'); beginvar i_z_drukuj : integer; var t_z_drukuj : tab; t_z_drukuj := t3; for i_z_generuj := 1 to n do write(t_z_drukuj [i]:8:5); writeln end { drukuj }; end.
<dekl_Procedury> ::=
function <Ident> [( <param_Formalne>) ] : <typ_prosty>;
<dekl_Stałych>
<dekl_Typów>
<dekl_Zmiennych>
<dekl_Procedur_lub_Funkcji>
begin
<Ciąg_Instrukcji>
end;
Idea jest taka, że ciąg instrukcji w ciele funkcji służyć ma
obliczeniu wartości, którą funkcja miałaby zwracać. W przekazaiu
wartości służy instrukcja przypisania pod nazwę funkcji tej wyliczonej
wartości. W związku z tym wywołanie funkcji typu, na przykład
Dla przykładu, w poniższym programie zadeklarowano rekursywną
funkcję
Kolorem podkreślono te instrukcje przypisania, które nadają nazwie funkcji wartość zwracaną w momencie jej wywołania.program Euklid; var k, l : integer; function NWD(m,n : integer) : integer ; begin if m = n then NWD := m else if m > n thenNWD := NWD(m-n,n) elseNWD := NWD(m,n-m) end { NWD }; begin readln(k,l); writeln('NWD(', k, ',', l, ')= ', NWD(k,l)) end.
Jak działał będzie ten program dla danych
program Euklid; var k, l, wynik : integer; function NWD(m,n : integer) : integer ; begin while m <> n do if m > n then m := m-n else n := n-m; NWD := m end { NWD }; begin readln(k,l); wynik := NWD(k,l); writeln('NWD(', k, ',', l, ')= ', wynik) end.
Jak działał będzie ten program dla danych
function NWD( var m,n : integer) : integer ;