APR, ćwiczenia 5

Procedury i funkcje

Procedury i funkcje to podprogramy – wydzielone fragmenty kodu, których można wygodnie używać w innych miejscach (wywoływać). W tym sensie są podobne do funkcji spotykanych w matematyce – podając argument nie musimy martwić się jak funkcja działa, wystarczy że wiemy co zrobić z wynikiem. Procedury to po prostu funkcje nie zwracające wyniku a jedynie wykonujące jakąś pracę.

Pascal potrzebuje jednak dokładnych informacji jak wywoływać daną funkcję i czego należy się spodziewać jako rozwiązania. Informacje te zawarte są w tzw. prototypie funkcji, który wygląda następująco:

function NazwaFunkcji(arg1: Typ1; arg2: Typ2): TypZwracany;

procedure NazwaProcedury(arg1: Typ1; arg2: Typ2);

W miejscu wywołania funkcji trzeba podać dokładnie tyle argumentów ile jest w prototypie, dodatkowo muszą być odpowiednich typów.

Same funkcje wyglądają jak małe programy, bo struktura funkcji jest następująca:

function NazwaFunkcji(arg1: Typ1; arg2: Typ2): TypZwracany;
var
    zmiennaLokalna1: Typ3;
    zmiennaLokalna2: Typ4;
begin
    { instrukcje }
end;

Instrukcje mogą być zupełnie dowolne, łącznie z wywołaniem innych funkcji czy nawet tej samej funkcji (nazywamy to rekurencją).

Dla przykładu zobaczmy jak mogłaby wyglądać funkcja obliczająca wartość bezwzględną:

function AbsoluteValue(x: Extended): Extended;
begin
    if x >= 0 then
        AbsoluteValue := x;
    else
        AbsoluteValue := -x;
end;

Jak widać, wartość funkcji zwracamy poprzez specjalną zmienną o nazwie takiej samej jak funkcja. Przypisanie wartości do tej zmiennej nie kończy funkcji, aby zakończyć wykonywanie funkcji zanim sterowanie osiągnie końcowe end, należy użyć polecenia exit.

Przekazywanie parametrów

Normalnie do funkcji i procedur przekazywane są tylko wartości argumentów, to znaczy, że ewentualna zmiana wartości argumentu w funkcji nie jest widoczna na zewnątrz. Dla przykładu weźmy poniższy program:

program NASA;

procedure Odliczanie(n: LongInt);
begin
    while n > 0 do
    begin
        WriteLn(n, '...');
        n := n - 1;
    end;
    WriteLn('START');
end;

var m: LongInt;
begin
    m := 3;
    Odliczanie(m);
    WriteLn(m);
end.

Po wykonaniu zobaczymy na ekranie:

3...
2...
1...
START
3

Aby funkcja mogła zmieniać zawartość przekazanego parametru trzeba to wyraźnie zadeklarować używając słowa kluczowego var przed nazwą parametru. Wtedy mówimy o przekazaniu przez referencję i zmiany wartości argumentu są widoczne dla kodu wywołującego. Ma to też inną konsekwencję – nie można przekazać funkcji w miejscu argumentu czegoś czego nie można zmieniać (np. liczby).

Drobna zmiana (dopisanie var przed argumentem procedury Odliczanie) daje:

3...
2...
1...
START
0

Rekurencja

Wyżej napisaliśmy, że jedna funkcja może wywoływać inne funkcje, także samą siebie. Oczywiście zadaniem programisty jest zapewnić, by każdy taki proces wywoływania się kiedyś zakończył (w przeciwnym przypadku dostaniemy błąd na etapie wywołania – zajęcie za dużej ilości pamięci w obszarze tzw. stosu). Dla przykładu, poniższa procedura choć skompiluje się bez problemu, nie jest poprawna, rekurencja nigdy się nie skończy:

procedure Praca(x: LongInt);
begin
    Praca(x);
end;

Z reguły funkcje rekurencyjne najpierw sprawdzają tzw. warunki początkowe – zatrzymujące w pewnym momencie rekurencję, potem wywołują same siebie z nieco zmienionymi parametrami. Dla przykładu, poniższa funkcja implementuje rekurencyjną wersję algorytmu Euklidesa:

function NWD(a: LongInt; b: LongInt): LongInt;
begin
    if b = 0 then
        NWD := a;
    else
        NWD := NWD(b, a mod b);
end;

Rekurencję można zawsze zamienić na iterację, choć nie zawsze jest to celowe. Dla przykładu elementy dobrze znanego ciągu Fibonacciego można liczyć następująco, sposób ten jest znacznie szybszy niż naiwna implementacja przy użyciu funkcji rekurencyjnej:

function F(n: LongInt): LongInt;
var F1, F2: LongInt;
begin
  F := 1;
  F1 := 1;
  while n > 0 do
  begin
      F2 := F + F1;
      F := F1;
      F1 := F2;
      n := n - 1;
  end;
end;

Zasięg zmiennych i podprogramów

Dość niecodzienną możliwością Pascala w stosunku do języków z rodziny C/C++ jest możliwość tworzenia zagnieżdżonych definicji procedur i funkcji. Podprogram można utworzyć wewnątrz innego (pod)podprogramu. Tak stworzony podprogram ma dostęp do wszystkich zmiennych podprogramu nadrzędnego, może też tworzyć własne zmienne. W przypadku nazwania kilku zmienncyh tą samą nazwą pierwszeństwo ma deklaracja znajdująca sie „najbliżej” pisanej instrukcji.

Zagnieżdżone podprogramy muszą zaczynać się i kończyć przed instrukcją begin podprogramu w którym są zagnieżdżone.

Powyższy mechanizm prowadzi do drzewa, w którym każdy podprogram ma swojego (jednego) rodzica. Mechanizm ten pozwala kilku funkcjom/procedurom dzielić te same zmienne, zdefiniowane w podprogramie – rodzicu. W tym kontekście pisany przez nas program jest po prostu podprogramem, który nie ma rodzica. Podprogram A może prawidłowo wywołać podprogram B tylko w trzech przypadkach:

program Nesting;

  procedure A;

    procedure B;

      procedure C;
      begin
          WriteLn('C');
      end;
    begin
        WriteLn('B');
        D; // Niedopuszczalne,
           // D jest rodzeństwem B, ale nie jest jeszcze zdefiniowane.
    end;

    procedure D;
    begin
        WriteLn('D');
        B; // Dopuszczalne,
           // B jest rodzeństwem D i jest już zdefiowane.
    end;

  begin
      WriteLn('A');
      B; //Dopuszczalne, B jest bezpośrednim potomkiem A.
      C; //Niedopuszczalne, C nie jest bezpośrednim potomkiem A.
  end;

  procedure AA(b: Boolean);

    procedure BB;
    begin
        WriteLn('BB');
        AA(False);
    end;

  begin
      if b then
          BB;
       WriteLn('AA');
  end;


begin
    A; //Dopuszczalne, A jest potomkiem naszego głównego programu.
    AA(True);
    AA(False);
end.

Zasięg zmiennych

Zmienne deklarowane w blokach var są widoczne i dostępne nie tylko dla (pod)programu, który zawiera blok var, ale także dla wszystkich podprogramów zagnieżdżonych. Zagnieżdżone podprogramy mają jednak dostęp tylko do tych zmiennych, które zostały zadeklarowane przed ich definicją. Ta relacja nie jest symetryczna – (pod)program nie ma dostępu do zmiennych podprogramu który został w nim zagnieżdżony.

program Scope;

  function F(x: LongInt): LongInt;
  var
      i: LongInt;
  begin
      i := x*x;
      F := i*i;
  end;

begin
    WriteLn(F(2));
    WriteLn(i); // Błąd, zmienna i należy do zagnieżdżonego podprogramu.
end.

Jeżeli podprogram zdefiniuje zmienną o takiej samej nazwie jak zmienna w podprogramie nadrzędnym, to ta nowa zmienna przesłoni w tym podprogramie tę z podprogramu nadrzędnego.

program Lokalne;
var
    x, y: LongInt;

  procedure P;
  var
      x: LongInt;
  begin
      x := 1;
      y := 1;
      WriteLn('x: ', x);
      WriteLn('y: ', y);
  end;

begin
    x := 2;
    y := 2;
    WriteLn('x: ', x);
    WriteLn('y: ', y);
    P;
    WriteLn('x: ', x);
    WriteLn('y: ', y);
end.

Powyższy mechanizm będzie bardzo pomocny w dalszej części semestru, gdy kilka funkcji będzie musiało działać na jednej tablicy.