Turbo Pascal. Programowanie

Turbo Pascal.
Programowanie

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

Copyright © 1996 by Wydawnictwo Helion
wersja spakowana

helion.pl

Jak program porozumiewa się z funkcją?

Jak powiedzieliśmy w poprzednim rozdziale, funkcja (procedura) pozwala zamknąć zestaw złożonych operacji w swoisty pojemnik, wykorzystywany przez wywołujący program na zasadzie czarnej skrzynki: po włożeniu do funkcji zestawu informacji, program odbiera wynik nie przejmując się sposobem jego uzyskania. Niestety, jak mogłeś się już przekonać, nasza skrzynka nie jest szczelna i stosunkowo łatwo doprowadzić do sytuacji, w której funkcja w sposób niekontrolowany "uszkodzi" zmienne programu. Zjawiska tego typu (zarówno kontrolowane, jak i niekontrolowane) zwane są efektami ubocznymi (ang. side effects) i dość często utrudniają życie mniej doświadczonym programistom. Na szczęście efektów ubocznych można łatwo uniknąć: pozwalają na to zmienne lokalne6.

Jak sama nazwa wskazuje, zmienna lokalna jest "prywatną własnością" funkcji lub procedury, wewnątrz której została zdefiniowana. Zmienna taka nie jest widoczna na zewnątrz funkcji (czyli np. w wywołującym ją programie), natomiast jest dostępna dla wszelkich funkcji i procedur zdefiniowanych w obrębie danej funkcji lub procedury (o ile ich definicje umieszczone są po deklaracji zmiennej). Przeciwieństwem zmiennej lokalnej jest oczywiście zmienna globalna, "widoczna" w całym obszarze programu od momentu jej zadeklarowania. Każda zdefiniowana w programie funkcja i procedura ma pełny dostęp do wszystkich zmiennych globalnych, czego jednak nie należy nadużywać (a wręcz nie należy używać w ogóle). "Zakres widzialności" zmiennej nazywany jest jej zasięgiem (ang. scope).

Zarówno deklaracja, jak i sposób wykorzystania zmiennych lokalnych są "na oko" takie same, jak dla zmiennych globalnych. Różnica kryje się w lokalizacji zmiennych obu typów: zmienne globalne przechowywane są w tzw. segmencie danych i istnieją przez cały czas wykonywania programu. Dzięki temu są zawsze dostępne dla każdej ze zdefiniowanych w programie funkcji, o ile nie zachodzi tzw. zjawisko przysłaniania, o którym za chwilę. Zmienne lokalne umieszczane są z kolei na stosie, czyli w specjalnym obszarze pamięci przeznaczonym do tymczasowego przechowywania informacji. W odróżnieniu od zmiennych globalnych, czas życia zmiennych lokalnych ograniczony jest do czasu wykonywania posiadającej je funkcji, tak więc "lokalność" ma charakter nie tylko przestrzenny, ale i czasowy - zmienne lokalne są tworzone w momencie rozpoczęcia wykonywania funkcji i znikają po jej zakończeniu. Oczywiste jest więc, że próba odwołania się do zmiennej lokalnej na zewnątrz deklarującej ją funkcji jest nielegalna, gdyż zmienna taka po prostu nie istnieje7.

Jak wcześniej wspomnieliśmy, funkcja (procedura) przypomina strukturą mały program, toteż deklaracje zmiennych lokalnych umieszcza się zwykle na jej początku, po nagłówku. Użycie zmiennych lokalnych zademonstrujemy na przykładzie programu Szlaczki, doprowadzając go przy okazji do stanu pełnej używalności. Aby to zrobić, wystarczy poprawić procedurę Szlaczek:

  procedure Szlaczek(Znak : char; Ile : integer);
   
  var  { deklaracja zmiennej lokalnej, }
    i : integer; { czyli licznika znaków }
   
  begin
    for i := 1 to Ile do { wypisz szlaczek }
      write(Znak); { złożony z iluś znaków }
    writeln
  end;

Tak poprawiony program powinien działać poprawnie niezależnie od żądanej długości ciągu znaków. Wprowadzając do procedury Szlaczek deklarację zmiennej i spowodowałeś, że podczas każdego wywołania korzysta ona nie ze zmiennej globalnej, ale ze swojej "prywatnej" zmiennej lokalnej. Zauważ przy okazji, że obie zmienne nazywają się tak samo, a mimo to program działa poprawnie. Zjawisko "maskowania" zmiennej globalnej przez identycznie nazwaną zmienną lokalną nazywa się przysłanianiem i daje Ci gwarancję, że nawet jeśli zadeklarujesz w funkcji zmienną lokalną o nazwie takiej samej jak zmienna zewnętrzna (w szczególności globalna), wszelkie odwołania wewnątrz funkcji będą zawsze kierowane do zmiennej lokalnej, zaś odpowiednia zmienna zewnętrzna pozostanie nienaruszona. Dzięki temu nie musisz dbać o to, by zmienne deklarowane w ramach poszczególnych funkcji miały różne nazwy i by któraś przypadkiem nie nazywała się tak samo, jak odpowiednia zmienna globalna.

Aby zobrazować pojęcia zasięgu i przysłaniania, wyobraźmy sobie pewien abstrakcyjny program, w którym zdefiniowano kilka zmiennych globalnych oraz procedur i funkcji posiadających swoje zmienne lokalne. Na rysunku obok przedstawiony jest schemat takiego programu. W tabelce pokazany jest zasięg poszczególnych zmiennych (widzialność w programie i procedurach), znak gwiazdki oznacza przysłanianie.

Zmienna
(właściciel)
Zasięg
Program
Pierwsza
Druga
Trzecia
Wewn
i (program)
tak
tak
tak
tak
tak
j (program)
tak
nie (*)
tak
tak
tak
x (program)
tak
tak
nie (*)
nie (*)
nie
j (Pierwsza)
nie
tak
nie
nie
nie
k (Pierwsza)
nie
tak
nie
nie
nie
x (Druga)
nie
nie
tak
nie
nie
k (Druga)
nie
nie
tak
nie
nie
x (Trzecia)
nie
nie
nie
tak
tak
k (Trzecia)
nie
nie
nie
tak
nie (*)
k (Wewn)
nie
nie
nie
nie
tak
l (Wewn)
nie
nie
nie
nie
tak

Rysunek 10. Widzialność zmiennych lokalnych i globalnych w programie

Kiedy używać zmiennych lokalnych, a kiedy globalnych? Zasadniczo, zmiennych globalnych w procedurach i funkcjach nie należy używać w ogóle. Zdarzają się jednak sytuacje, kiedy świadome użycie zmiennej globalnej (czyli świadome wykorzystanie efektów ubocznych) upraszcza program. Efekty uboczne znajdują na ogół zastosowanie do przekazywania informacji z i do funkcji (procedur), pozwalając na zredukowanie liczby parametrów. Jeśli jednak chciałbyś zaoszczędzić sobie pisania deklarując w programie jedną zmienną globalną i i używając jej jako licznika we wszystkich procedurach, które takowego potrzebują, szukasz guza. Prędzej czy później któraś z przyszłych modyfikacji programu spowoduje ujawnienie się efektów ubocznych w formie prowadzącej do błędu wykonania.

Ponieważ wszelkie czynności wykonywane przez funkcję lub procedurę są niedostrzegalne dla wywołującego ją programu, zmienne wykorzystywane w trakcie wykonywania tych czynności powinny być dla niego niedostrzegalne, a zatem powinny być deklarowane jako zmienne lokalne. W szczególności dotyczy to zmiennych tymczasowych i pomocniczych, jak liczniki pętli, tymczasowe wartości maksimów i minimów, zmienne tekstowe używane przy wyprowadzaniu komunikatów itp.

Wszelkie zmienne przeznaczone wyłącznie do użytku wewnętrznego funkcji i procedur powinny być bezwzględnie deklarowane jako zmienne lokalne. Zmienne globalne powinny być wykorzystywane tylko do przechowywania danych istniejących przez cały czas trwania programu.

W ostatniej części tego rozdziału zajmiemy się sprawą przekazywania informacji pomiędzy funkcjami i procedurami a wywołującym je programem. Jak już wiesz, wymiana taka może być zrealizowana z wykorzystaniem efektów ubocznych (co nie jest wskazane) lub za pomocą parametrów (argumentów). Deklarowanie i wykorzystanie parametrów było już omawiane poprzednio, przypomnijmy więc w skrócie:

Wewnątrz funkcji parametry zachowują się jak normalne zmienne (można wykonywać na nich wszelkie operacje tak samo, jak na zmiennych lokalnych). Możliwe jest zadeklarowanie parametru o nazwie takiej samej, jak zmienna globalna (w ogólności - zmienna zadeklarowana w bloku nadrzędnym); wystąpi wówczas efekt przysłaniania, tj. wszelkie odwołania wewnątrz funkcji korzystającej z parametru będą odnosiły się do niego, a nie do przysłoniętej przezeń zmiennej. Nielegalne jest natomiast deklarowanie zmiennych lokalnych i parametrów o identycznych nazwach (nie mówiąc już o zadeklarowaniu parametru o nazwie identycznej z nazwą funkcji, która go wykorzystuje).

Wszystkie identyfikatory (tj. nazwy zmiennych, stałych, typów, parametrów, procedur i funkcji) deklarowane w obrębie danego bloku (z wyjątkiem bloków podrzędnych) muszą być unikalne. Możliwe jest natomiast "zdublowanie" identyfikatora w bloku podrzędnym, co spowoduje efekt przysłaniania.

Warto wspomnieć, że początkujący programiści często nadużywają parametrów, wykorzystując je jako zmienne lokalne lub (co jest już zbrodnią) deklarują dodatkowe parametry tylko i wyłącznie w tym celu. Praktyka ta, chociaż legalna, jest sprzeczna z zasadami poprawnego programowania: parametry przeznaczone są do wymiany informacji pomiędzy funkcją, nie zaś do "podpierania się" w wykonywanych przez nią operacjach o charakterze wewnętrznym.

Tworząc program musisz pamiętać o właściwym podziale kompetencji pomiędzy składającymi się nań obiektami. Jeśli obiekt przeznaczony jest do przekazywania informacji, nie powinieneś używać go jako zmiennej lokalnej. Poczta nie służy do przesyłania wiadomości do sąsiedniego pokoju.

Do tej pory koncentrowaliśmy się na przekazywaniu informacji z wywołującego programu do funkcji lub procedury. Po przetworzeniu tej informacji na ogół zachodzi konieczność zwrócenia wyniku do wywołującego. Jeśli wynik jest pojedynczą wartością (jest typu prostego), najlepiej użyć funkcji, która pozwala na zwrócenie go przez nazwę. Rozwiązanie to bywa jednak czasami niewygodne, nie mówiąc już o sytuacjach, gdy trzeba zwrócić do wywołującego dwie lub więcej wartości. Ponieważ trzymamy się z daleka od efektów ubocznych, pozostaje wykorzystanie do tego celu parametrów. Wyobraź sobie na przykład, że jest Ci potrzebna funkcja obliczająca jednocześnie sinus i cosinus danego kąta. Można to zrobić np. tak:

  program Trygonometria;
  { demonstracja przekazywania parametrów }
   
  var
    i : integer;
    s, c : real;
   
  procedure SinCos(x : real; SinX, CosX : real);
  { oblicza jednocześnie sinus i cosinus argumentu }
   
  begin
    SinX := sin(x);
    CosX := cos(x);
  end;
   
  begin
    SinCos(Pi/3,s,c); { oblicz wartości }
    writeln(s:8:3, c:8:3); { i wypisz je na ekranie }
  end.

Niestety, wynik bynajmniej nie jest satysfakcjonujący: zamiast obliczonych wartości program wyświetla zera lub inne pozbawione sensu liczby. Dzieje się tak dlatego, iż w swojej obecnej formie parametry SinX i CosX pozwalają jedynie na przekazanie informacji do procedury, nie zaś odwrotnie. Na szczęście wystarczy jedna drobna modyfikacja w programie:

procedure SinCos(x : real; var SinX, CosX : real);

by wszystko zaczęło działać poprawnie. Co takiego się zmieniło?

Otóż dodanie słowa kluczowego var przed nazwą parametru (parametrów) zasadniczo zmienia sposób ich przekazywania. Poprzednio mieliśmy do czynienia z tak zwanym przekazywaniem przez wartość, umożliwiającym wyłącznie włożenie danych do funkcji (procedury). Działanie przekazywania przez nazwę sprowadzało się do umieszczenia w odpowiednim miejscu (tj. na stosie) kopii wartości parametru aktualnego, która następnie mogła być przetwarzana wewnątrz funkcji lub procedury. Obecnie wykorzystywany mechanizm nosi nazwę przekazywania przez nazwę (inne mądre określenie to przekazywanie przez wskaźnik lub przez referencję) i różni się od poprzedniego tym, że procedura otrzymuje nie kopię wartości parametru aktualnego, lecz informację o jego położeniu. Można to przedstawić następująco:

Rysunek 11. Mechanizm przekazywania parametrów: a) przez wartość, b) przez nazwę

W przypadku przekazywania przez wartość funkcja otrzymuje jedynie kopię rzeczywistego parametru. Zasięg operacji dokonywanych na takiej kopii ogranicza się do wnętrza funkcji, zaś gdy ta kończy działanie, wartość parametru jest tracona. Parametry przekazywane przez wartość zachowują się więc identycznie jak zmienne lokalne, różniąc się od nich jedynie przeznaczeniem. Inaczej wygląda sprawa w przypadku przekazywania przez nazwę: funkcja otrzymuje tu nie kopię parametru, lecz tzw. wskaźnik opisujący jego rzeczywiste położenie. Wszelkie operacje dokonywane przez funkcję na parametrze przekazywanym przez nazwę dotyczą nie kopii, ale rzeczywistego obiektu będącego własnością programu. Można więc powiedzieć, że funkcja "sięga na zewnątrz" do zasobów programu i modyfikuje je ze skutkiem natychmiastowym, a efekty wszystkich operacji pozostają "utrwalone" w treści obiektu będącego parametrem. Zauważ, że ponieważ funkcja otrzymuje tu wskaźnik do rzeczywistego parametru, nie możesz przekazać przez nazwę obiektu nie posiadającego lokalizacji w pamięci (np. stałej czy wyrażenia), czyli nie będącego l-wartością. Przekazanie takiego parametru przez wartość jest natomiast najzupełniej legalne.

Kiedy stosować przekazywanie parametrów przez wartość, a kiedy przez nazwę? Przekazywanie przez wartość używane jest w "komunikacji jednokierunkowej", czyli wówczas, gdy zwrócenie wartości do wywołującego programu nie jest wymagane lub wręcz jest niewskazane (tj. chcemy zabezpieczyć się przed modyfikacją obiektu będącego parametrem). Jeżeli zachodzi potrzeba przekazania wartości z powrotem do wywołującego, konieczne jest użycie przekazywania przez nazwę, czyli poprzedzenie nazwy parametru słowem var. Przekazywanie przez nazwę stosuje się również dla argumentów o większych rozmiarach (np. tablic, łańcuchów i innych zmiennych strukturalnych, o których powiemy wkrótce), nawet wówczas, gdy nie chcemy zwracać ich wartości. Jak powiedzieliśmy przed chwilą, przekazanie przez wartość powoduje umieszczenie na stosie kopii parametru, co przy większych rozmiarach parametrów zajmuje sporo czasu i może doprowadzić do przepełnienia stosu (na którym oprócz tego muszą zmieścić się zmienne lokalne i kilka innych rzeczy). Podczas przekazywania przez nazwę na stosie umieszczany jest zawsze czterobajtowej długości wskaźnik do parametru (niezależnie od faktycznej wielkości tego ostatniego), co oczywiście trwa znacznie krócej. Aby zabezpieczyć taki parametr przed niepożądaną modyfikacją, możemy zastosować w miejsce słowa var słowo kluczowe const, które również powoduje przekazanie wskaźnika do parametru, blokując jednocześnie możliwość modyfikowania tego ostatniego. Aby się o tym przekonać, spróbuj zmierzyć czas wykonania następującego programu:

  program Parametry;
   
  type
    TablInt = array [1..5000] of integer;
   
  var
    i : integer;
    ti : TablInt;
   
  procedure Pusta(t : TablInt);
   
  var k : integer;
   
  begin
    k := t[1]
  end;
   
  begin
    for i := 1 to 1000 do { to może potrwać! }
      Pusta(ti);
  end.

Jeśli już to zrobiłeś, zmień nagłówek procedury Pusta na

procedure Pusta(var t : TablInt)

Tym razem program powinien wykonać się błyskawicznie (przynajmniej w porównaniu do poprzedniej wersji). Dzieje się tak dlatego, że podczas każdego wywołania procedury na stosie umieszczany jest jedynie wskaźnik do tablicy t (w sumie tysiąc razy po 4 bajty), podczas gdy poprzednio na stos kopiowana była cała tablica (tysiąc razy 10 000 bajtów, czyli 2500 razy więcej). Dodatkową "atrakcją" związaną z przekazywaniem większych struktur przez wartość jest możliwość przepełnienia stosu (standardowo wielkość segmentu stosu ustawiana jest przez Turbo Pascala na 16 kB). Aby się o tym przekonać, zmień w pierwotnej wersji programu (tej bez słowa var w nagłówku procedury) rozmiar tablicy na 10000 liczb:

  type
    TablInt = array [1..10000] of integer;

Próba wykonania programu skończy się tym razem komunikatem Stack overflow error: sumaryczny rozmiar tablicy wynosi 20000 bajtów (10000 dwubajtowych liczb całkowitych), czyli więcej niż można umieścić na stosie. Podobnie jak poprzednio, przekazanie parametru przez nazwę rozwiązuje problem.

Przekazując większe struktury danych jako parametry funkcji i procedur należy unikać przekazywania przez wartość i stosować w zamian przekazywanie przez nazwę. Aby zabezpieczyć się przed przypadkową modyfikacją wartości parametru, można zastosować słowo kluczowe const w miejsce var.

W ten sposób zakończyliśmy omawianie podstawowych zagadnień związanych z procedurami i funkcjami. W następnych rozdziałach zajmiemy się wyjaśnianiem dwóch nowinek, które pojawiły się w ostatnim programie, i które zapewne przyjąłeś na wiarę. Są to mianowicie tablice (a właściwie typy strukturalne) oraz definiowanie własnych typów, czyli słowo kluczowe type.

Zapamiętaj

Przypisy

6. Ściślej, obiekty lokalne, bowiem oprócz zmiennych lokalne mogą być również stałe i typy, o których jednak na razie nie mówiliśmy. | wróć |

7. Wyjątkiem są tutaj lokalne zmienne predefiniowane (zmienne z wartością początkową), które lokowane są w segmencie danych, a więc istnieją przez cały czas wykonywania programu. Nie zmienia to faktu, że i one nie są widoczne na zewnątrz posiadających je funkcji i procedur. | wróć |

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