Podstawy Programowania Wyk ad czwarty: ł Funkcje i procedury.
Transkrypt
Podstawy Programowania Wyk ad czwarty: ł Funkcje i procedury.
Podstawy Programowania Wykład czwarty: Funkcje i procedury. 1. Podprogramy Podprogramy są wyodrębnionymi fragmentami programu komputerowego. Istnieją dwa główne powody dla których je stosujemy. Tworząc duży program komputerowy stwierdzamy dosyć często, że wygodnie by było użyć fragmentu kodu, który już wcześniej napisaliśmy. Najprostsze rozwiązanie tego problemu, polegające na skopiowaniu go nie jest rozwiązaniem najlepszym. Jeśli ów fragment zawierał 10 błędów, to teraz mamy 20 błędów, które trzeba usunąć1. Najlepszym rozwiązaniem jest zatem zamknięcie tego kod w podprogramie. Powtórne użycie tego kodu będzie więc polegało na wywołaniu podprogramu za pomocą jego nazwy. Drugim powodem stosowania podprogramów jest to, że pozwalają one podzielić kod na niezależne jednostki zwiększając jego czytelność. Specjaliści od teorii programowania mówią o „budowaniu abstrakcji wyższego rzędu”. Rzeczywiście, mając zapisany w języku programowania algorytm liczenia silni, można go zamknąć w podprogramie o nazwie „silnia” lub „licz_silnia”. Jeśli w programie napotkamy teraz instrukcję „licz_silnia(n)”, to łatwiej jest nam zrozumieć co ona wykonuje, niż gdybyśmy musieli prześledzić wykonanie samego kodu. Program podzielony na podprogramy jest zatem prostszy do analizowania i łatwiej jest w nim usuwać błędy. W języku Pascal istnieją dwa rodzaje podprogramów – funkcja i procedura. 2. Ogólna struktura procedur i funkcji Tworzenie funkcji lub procedury nazywa się jej definiowaniem. Definicja procedury zaczyna się nagłówkiem składającym się ze słowa kluczowego procedure, po którym występuje nazwa (identyfikator) procedury2. Po nazwie opcjonalnie może wystąpić lista parametrów formalnych. Całość zakończona jest średnikiem. Po nagłówku procedury występuje ciało procedury3. Jego struktura przypomina strukturę programu głównego. Składa się ono z części opisowej i z bloku instrukcji, które będą w procedurze wykonywane. Część opisowa procedury może, podobnie jak część opisowa programu zawierać definicje stałych, typów, deklaracje zmiennych i etykiet, a także definicje innych procedur i funkcji. Nie może natomiast zawierać sekcji zaczynającej się słowem kluczowym uses i pozwalającej na włączanie do programu modułów. Wszystkie elementy występujące w części opisowej procedury są widoczne tylko wewnątrz tej procedury, tzn. jeśli zadeklarujemy jakąś stałą wewnątrz 1 Programiści wymyślili nawet regułę, która przypomina, żeby czegoś takiego nie robić DRY (Don't Repeat Yourself). 2 Zaleca się aby nazwa ta była czasownikiem lub zawierała czasownik, ale nie jest to wymóg. 3 Termin ciało procedury (lub ciało funkcji) może oznaczać również instrukcje zawarte w podprogramie między słowami kluczowymi begin i end. 2 procedury i będziemy próbować użyć jej w bloku głównym programu, to taki program się nie skompiluje. Te elementy nazywamy elementami lokalnymi procedury. Blok w którym są zawarte instrukcje wykonywane w procedurze rozpoczyna się słowem kluczowym begin, a kończy słowem end zakończonym średnikiem. Definicja funkcji4 jest podobna do definicji procedury, ale są różnice. Funkcje zawsze zwracają wynik swojego działania. Nagłówek funkcji, w przeciwieństwie do nagłówka procedury zaczyna się słowem kluczowym function, a po nazwie funkcji i ewentualnej liście parametrów umieszczany jest dwukropek i nazwa typu wartości przez funkcję zwracanej. W bloku instrukcji musi się znaleźć instrukcja przypisania, gwarantująca, że ta wartość zostanie rzeczywiście zwrócona. Jest ona tworzona według schematu: nazwafunkcji := wartość; Poniżej umieszczone są przykłady prostej procedury i funkcji: procedure wypisz; begin clrscr; writeln('Prosta procedura.'); readln; end; Do najprostszych procedur5 należą procedury bez parametrów. Aby program, w którym procedura wypisz jest umieszczona skompilował się prawidłowo, musi być do niego włączony moduł crt, w którym jest zdefiniowana procedura clrscr (jest ona również procedurą bez parametrów, ale jest to procedura predefiniowana – dostarczona nam przez twórców kompilatora Pascala). Zastosowanie procedur bezparametrowych powinno być ograniczone do prostych czynności, takich jak wypisywanie komunikatów na ekran. Należy zauważyć, że wszystkie instrukcje, które zostały umieszczone w bloku instrukcji omawianej procedury są wywołaniami procedur stanowiących podstawowe elementy języka Turbo Pascal. function znak:char; begin znak:='a'; end; Funkcja w ramce zwraca ustaloną wartość (znak a). Jeśli umieścilibyśmy po instrukcji przypisania znak:='a'; inne instrukcje, to również zostaną one 4 Funkcje z języka Pascal pod pewnymi względami przypominają funkcje w matematyce. 5 Pod względem konstrukcji, a nie działania. 3 wykonane. Jeśli zapomnimy o instrukcji przypisania, to funkcja zwróci przypadkowy znak, a nie znak o kodzie ASCII równym 97 (litera a). Funkcje mogą zwracać wartości typów podstawowych, nie mogą natomiast zwracać bezpośrednio wartości typów złożonych. 3. Wywołanie i wykonywanie procedur i funkcji Aby skorzystać w programie (lub innej procedurze lub funkcji) z procedury należy ją w nim wywołać. W przypadku procedury wypisz, wystarczy umieścić w programie jej nazwę zakończoną średnikiem. Miejsce w programie, gdzie procedura lub funkcja jest wywoływana nazywamy po prostu miejscem wywołania. Po napotkaniu wywołania procedury program wykonuje skok do miejsca, gdzie w pamięci została ona umieszczona i wykonuje ją. To wykonanie zaczyna się od miejsca oznaczonego słowem kluczowym begin, które nazywane jest „punktem wejścia sterowania”. Po wykonaniu procedury program skacze do punktu znajdującego się bezpośrednio za miejscem jej wywołania i wykonuje następną w kolejności instrukcję. Funkcję możemy wywołać w ten sam sposób, ignorując wartość przez nią zwracaną6, ale najczęściej wywołuje się ją w sposób pozwalający zachować tę wartość w jakiejś zmiennej. Załóżmy, że w programie mamy zadeklarowaną zmienną globalną typu char, o nazwie zm. Wywołanie funkcji znak może mieć postać: zm:=znak; Funkcję można również wywołać w wyrażeniu, np.: n*sin(x). 4. Zmienne lokalne W części opisowej podprogramu możemy definiować i deklarować elementy, które będą widoczne tylko wewnątrz niego. Są to elementy lokalne, ukryte przed otoczeniem podprogramu. W przypadku stałych, typów danych i etykiet mechanizm ukrywania jest prosty – wystarczy, aby kompilator zadbał o to by ich nazwy nie pojawiały się poza podprogramem. W przypadku zmiennych sytuacja jest trochę bardziej skomplikowana, bo kompilator musi przydzielić na nie miejsce w pamięci. Pamięć, która zostaje przydzielona skompilowanemu programowi komputerowemu w celu jego uruchomienia dzieli się na trzy obszary7. W pierwszym obszarze znajdują się rozkazy programu, w drugim dane dla tych rozkazów, a trzeci jest przeznaczony na tak zwany stos. Informacje na stosie są przechowywane zgodnie z regułą LIFO (ang. Last In First Out), co można przetłumaczyć jako: „ostatni nadszedł, pierwszy wyszedł”. Załóżmy, że naszym podprogramem jest procedura. W momencie jej wywołania na stosie 6 Taki sposób wywołania funkcji nazywa się w literaturze „wywołaniem funkcji ze względu na efekt uboczny jej działania”. 7 Jest to uproszczony opis problemu, rzeczywistość jest trochę bardziej skomplikowana. 4 rezerwowana jest pamięć na tak zwaną ramkę stosu8. Ta ramka zawiera miejsce na zmienne lokalne, parametry procedury i adres powrotu. Wszystkie wartości zapisywane w zmiennych lokalnych są więc umieszczane na stosie w opisywanej ramce. Po zakończeniu działania procedury ramka jest usuwana ze stosu. Załóżmy, że w procedurze jest wywoływana inna procedura lub funkcja. W takim przypadku, za ramką stosu pierwszej procedury tworzona jest ramka procedury w niej wywołanej. Po zakończeniu wykonania drugiej procedury jest niszczona jej ramka, a sterowanie wraca do pierwszej procedury. Na podstawie tego opisu łatwo zauważyć, że zmienne lokalne istnieją w pamięci komputera tylko podczas wykonywania procedury, w której zostały zadeklarowane, dlatego też nazywane są zmiennymi automatycznymi. Pamięć na te zmienne nie jest zerowana, tak więc nie są one domyślnie inicjalizowane żadną wartością9. Wewnątrz podprogramu nie możemy zadeklarować dwóch zmiennych o takich samych nazwach, ale możemy mieć zmienną lokalną, o takiej samej nazwie, jak zmienna globalna. Wewnątrz podprogramu będzie widoczna tylko zmienna lokalna, natomiast nie będziemy mieli bezpośredniego dostępu do zmiennej globalnej o takiej samej nazwie. To zjawisko nazywa się przykrywaniem i dotyczy nie tylko zmiennych ale również innych elementów lokalnych. Ponieważ zmienne lokalne pozwalają na lepszą gospodarkę pamięcią komputera (istnieją tylko wtedy, kiedy są potrzebne), to zaleca się ograniczanie liczby zmiennych globalnych, na rzecz zmiennych lokalnych. 5. Parametry procedur i funkcji Aby wykonywać jakąś użyteczną pracę, nie tylko polegającą na wyświetlaniu komunikatów na ekranie, podprogramy muszą mieć możliwość wpływania na stan zmiennych globalnych. Funkcje i procedury mogą bezpośrednio się do nich odwoływać. Załóżmy, że mamy zadeklarowane w programie dwie zmienne globalne o nazwach a i b oraz typie byte. Procedura, która pobiera od użytkownika pewne wartości i umieszcza je w tych zmiennych mogłaby mieć następującą postać: procedure pobierz; begin writeln('Podaj dwie liczby z przedziału [0,255]'); readln(a); readln(b); end; Bezpośrednie korzystanie ze zmiennych globalnych, choć dopuszczalne 8 W niektórych opracowaniach zwaną także rekordem aktywacyjnym podprogramu. 9 Dokładniej – mogą zawierać przypadkową wartość. 5 i w niektóry (bardzo rzadkich) sytuacjach nieuniknione jest uważane za złą technikę programowania i nie należy tego robić10. Jeśli chcemy wykonać operacje na wartości znajdującej się w zmiennej, wówczas musimy w nagłówku procedury lub funkcji umieścić parametry11, przez które te wartości zostaną przekazane. Parametry są zmiennymi lokalnymi12, które pełnią ważną rolę – ich zadaniem jest komunikacja z otoczeniem procedury lub funkcji. Są one umieszczane w nawiasach okrągłych, na liście parametrów, tuż za nazwą procedury lub funkcji. Deklarując parametr podajemy jego nazwę i typ. Jeśli chcemy zadeklarować dwa lub większą liczbę parametrów, to rozdzielamy je średnikami. Jeśli parametry mają być tego samego typu, to wymieniamy ich nazwy rozdzielając je przecinkami, a potem, po dwukropku podajemy ich typ. Oto przykładowy nagłówków z parametrami dla funkcji i procedury: procedure wzor(a,b:real; x:byte); function wzor(a,b:real; x:byte):char; Parametry znajdujące się na liście parametrów funkcji lub procedury nazywamy parametrami formalnymi. W miejscu wywołania procedury lub funkcji należy za te parametry podstawić parametry faktyczne13. Tymi parametrami mogą być np.: zmienne globalne programu lub zmienne lokalne procedury, w której dany podprogram został wywołany, o ile mają one typy zgodne z typami parametrów. Ponadto parametrów faktycznych musi być tyle samo ile jest parametrów formalnych. Powyższe objaśnienie nie do końca jest precyzyjne. Istnieją bowiem trzy sposoby przekazania parametrów faktycznych. Pierwszy z nich to przekazanie przez wartość. Parametry formalne, które zostały zadeklarowane wyżej pozwalają właśnie na taki typ przekazania. Parametrem faktycznym, przekazanym przez wartość może być zmienna, wyrażenie lub wartość, o typie zgodnym z typem parametru. W takim przypadku wartość parametru faktycznego jest kopiowana do parametru formalnego. Wewnątrz funkcji lub procedury parametr ten może być traktowany jak zwykła zmienna, tzn. można go odczytywać lub zmieniać jego wartość. Po wykonaniu podprogramu ten parametr, tak jak zwykłe zmienne 10 Niektóry informatycy przyjmują bardziej skrajną postawę i zalecają, aby w ogóle nie stosować zmiennych globalnych. Jest to oczywiście możliwe w językach programowania, o trochę innej filozofii tworzenie programów niż ma to miejsce w Pascalu, ale dobrą praktyką jest stosowanie w programach napisanych w języku Pascal jak najmniejszej liczby zmiennych globalnych. 11 Wymiennie możemy używać określenia „argumenty”. 12 W związku z tym nie mogą mieć takich samych nazw jak inne zmienne lokalne. 13 Zwane także argumentami wywołania. W literaturze polskiej funkcjonuje pojęcie „parametrów aktualnych”, które wydaje się być niezbyt udanym tłumaczeniem angielskich słów actual parameters. Bardziej odpowiednim tłumaczeniem byłoby „parametry właściwe”. 6 lokalne jest usuwany z pamięci wraz z zawartością. Jeśli nie chcemy jej stracić, to musimy ją w jakiś sposób „utrwalić”, np.: wypisując na ekran. Drugim sposobem jest przekazanie przez stałą. Parametr formalny pozwalający na takie przekazanie jest poprzedzony słowem kluczowym const. Pod ten parametr możemy również podstawiać wartość, wyrażenie lub zmienną, ale wewnątrz procedury nie można zmieniać jego wartości, jest on tylko do odczytu. Ostatnim sposobem przekazania jest przekazanie przez zmienną, zwane również przekazaniem przez adres. W tym przypadku parametr formalny pozwalający na takie przekazanie jest poprzedzony słowem kluczowym var. Jako parametr faktyczny może być za niego podstawiona wyłącznie zmienna. Parametrowi formalnemu nie jest przypisywana wartość zmiennej, która za niego została podstawiona w miejscu wywołania podprogramu, ale adres tej zmiennej. Oznacza, to że wszelkie modyfikacje wartości parametru będą również modyfikacjami wartości tej zmiennej i te modyfikacje zostaną zachowane po zakończeniu podprogramu. W przypadku przekazania przez wartość, zmienna podstawiona pod parametr formalny po zakończeniu podprogramu ma taką samą wartość jak przed jego rozpoczęciem, niezależnie od tego co działo się z parametrem formalnym wewnątrz podprogramu. Parametry przekazywane przez wartość i stałą pełnią więc rolę wyłącznie parametrów wejściowych podprogramu, natomiast parametry przekazywane przez zmienną są zarówno parametrami wejściowymi, jak i wyjściowymi. Deklarując parametry należy pamiętać, że jeśli chcemy przekazać do procedury lub funkcji parametr faktyczny o dużej objętości, jak np.: zmienną typu string, to należy przekazać go przez stałą lub zmienną. Nie powinniśmy przekazywać takich zmiennych przez wartość, ponieważ wartość tych zmiennych jest kopiowana na stos, który zajmuje stosunkowo mało pamięci i możemy go przepełnić powodując awaryjne zakończenie programu. Jeśli deklarujemy parametry formalne, typów single, double, extended oraz comp i przekazujemy je przez stałą lub zmienną, to możemy takich parametrów zadeklarować maksymalnie osiem. Parametry pozwalają uczynić podprogramy bardziej uniwersalnymi i elastycznymi. Dobrze sparametryzowany podprogram14 może być stosowany do rozwiązywania wielu podobnych do siebie zagadnień. Parametry wraz z nazwą stanowią interfejs podprogramu. Jeśli programista korzysta z procedur i funkcji napisanych przez innych programistów, to nie musi znać szczegółów ich działania, wystarczy, żeby wiedział, co one robią i jak należy je wywołać, tj. jakie przekazać im argumenty wywołania. Poniżej znajdują się przykłady procedur, które stosują różne sposoby przekazania parametrów. 14 Nie oznacza, to że taki podprogram musi zawierać dużą liczbę parametrów. Zbyt duża ich liczba może być równie szkodliwa, co ich brak. 7 Przez wartość: procedure wyswietl(x,y:byte); begin writeln('W procedurze - przed wykonaniem zmian: ',x:3,y:3); x:=x+3; y:=y-1; writeln('W procedurze - po dokonaniu zmian: ',x:3,y:3); end; Przez stałą: procedure wyswietl2(const x,y:byte); begin writeln('W procedurze - nie można zmienić wartości parametrów ',x+2:3,y-1:3); {x:=x+1; to się nie skompiluje} end; Przez zmienną: procedure wyswietl3(var x,y:byte); begin writeln('W procedurze - przed dokonaniem zmian: ',x:3,y:3); x:=x+1; y:=y-1; writeln('W procedurze - po wykonaniu zmian: ',x:3,y:3); end; Można oczywiście w procedurze równocześnie przekazywane przez stałą, zmienną i przez wartość. zadeklarować parametry 5. Deklaracja procedur i funkcji W procedurach i funkcjach możemy korzystać z elementów, które zostały zdefiniowane lub zadeklarowane przed ich definicjami. Co jednak zrobić jeśli chcemy wywołać w procedurze inną procedurę, ale nie chcemy z jakiś powodów na razie jej definiować? Możemy w takim wypadku procedurę zadeklarować, podając jej nagłówek, a następnie po jej nagłówku umieszczając słowo kluczowe forward zakończone średnikiem15. Tak zadeklarowaną funkcję lub procedurę należy oczywiście kiedyś zdefiniować, niemniej jednak możemy ją wywoływać (na etapie pisania programu, nie zaś wykonania), zanim zostanie ona zdefiniowana. 15 W literaturze taka deklaracja procedury lub funkcji jest czasem nazywana deklaracją wyprzedzającą. 8 6. Zagnieżdżone funkcje i procedury. Dowiedzieliśmy się, że zmienne lokalne są ukryte dla elementów programu znajdujących się na zewnątrz podprogramu. Język Pascal pozwala również na umieszczanie wewnątrz procedur i funkcji definicji innych procedur i funkcji, które są lokalne, tzn. widoczne wyłącznie w podprogramie, w którym zostały zdefiniowane. Takie procedury i funkcje nazywamy zagnieżdżonymi. Mają one dostęp do wszystkich składowych funkcji lub procedury, w której zostały zdefiniowane, natomiast funkcja lub procedura otaczająca je nie ma dostępu do ich elementów lokalnych. Stosowanie zagnieżdżonych procedur i funkcji pozwala na ukrywanie implementacji, a więc ukrywanie szczegółów związanych z funkcjonowaniem danego podprogramu. 7. Uwagi na temat pisania funkcji i procedur Definicja funkcji i procedury nazywana inaczej jej treścią powinna być niewielka. Dobrze napisaną funkcję lub procedurę powinno dać się wyświetlić na raz na ekranie. Jeśli jej treść się nie mieści na ekranie, to należy rozważyć podzielenie jej na mniejsze części. Dobrym zwyczajem jest umieszczanie po nagłówku funkcji lub procedury komentarzy objaśniających, co robi dany podprogram i do czego służą ich parametry. Należy również pamiętać o wcięciach ułatwiających czytanie kodu. To czy dany fragment programu zostanie zaimplementowany jako funkcja, czy jako procedura zależy wyłącznie od programisty16, ale ważnym jest, aby te podprogramy były dobrze sparametryzowane. Istnieje pewien ciekawy sposób pisania funkcji, który wywodzi się z innego języka programowania, języka C. Otóż funkcja może wykonywać działania, których wyniki są zwracane do parametrów faktycznych przekazywanych przez zmienne, natomiast wartość funkcji oznacza status wykonania tych działań. Wartość zero, najczęściej oznacza, że wyniki są poprawne, wartości różne od zera oznaczają, że wystąpił jakiś błąd. Pojedyncza wartość sygnalizuje pojedynczy rodzaj błędu. 8. Uwagi dotyczące pisania złożonych programów Języki pozwalające na definiowanie podprogramów, takie jak Pascal, nazywamy językami strukturalnymi. Pozwalają one pisać programy na zasadzie: „od ogółu do szczegółu”, tzn. problem, który program ma rozwiązać dzielimy na mniejsze podproblemy, aż będziemy w stanie rozwiązać każdy z tych podproblemów i to 16 Programiści piszący tak zwane programy obiektowe zalecają, aby stosować głównie funkcje, a parametry do nich przekazywać wyłącznie przez stałe. Niestety w języku Pascal takie podejście nie jest możliwe. 9 rozwiązanie zaimplementować w postaci podprogramu. Następnie ze zdefiniowanych podprogramów, możemy „złożyć” cały program17. Prześledźmy to postępowanie na przykładzie prostego programu. Załóżmy, że chcemy napisać program obliczający silnię, który będzie sprawdzał poprawność wprowadzonych przez użytkownika danych wejściowych. Narzucającym się rozwiązaniem jest umieszczenie obliczeń w funkcji, a komunikacji z użytkownikiem i walidacji danych wejściowych w procedurze. Po bliższej analizie problemu możemy dojść do wniosku, że proces walidacji danych można podzielić na dwa etapy: sprawdzenie czy podana wartość jest liczbą i sprawdzenie, czy ta liczba mieści się w określonym zakresie. Pierwszą czynność można zaimplementować w postaci procedury zagnieżdżonej w procedurze wykonującej czynność drugą (procedura sprawdzająca, czy podane przez użytkownika dane są poprawne nie będzie wykorzystywana przez inne elementy programu, dlatego może być przed nimi ukryta). Oto kod programu, który powstał na bazie powyższego opisu: program licz_silnie; uses crt; var n:byte; procedure pobierz_dane(var x:byte); procedure sprawdz(var x:byte); var s:string; blad:integer; begin repeat writeln('Wprowadź liczbę naturalna z przedziału [0,8]'); readln(s); clrscr; val(s,x,blad); if blad<>0 then writeln('Błąd, to nie jest liczba!'); 17 Postępowanie to wywodzi się od sposobu rozwiązywania problemów z innych dziedzin (głownie matematyki), który podał Kartezjusz. 10 until blad=0; end; begin repeat sprawdz(x); if x>8 then writeln('Liczba nie należy do podanego przedziału'); until x<=8; end; function silnia(n:byte):word; var i:byte; s:word; begin s:=1; for i:=1 to n do s:=s*i; silnia:=s; end; begin clrscr; pobierz_dane(n); writeln('Wartość silni dla n=',n:1,' wynosi: ',silnia(n),'.'); readln; end. Po przeanalizowaniu wszystkich zalet stosowania podprogramów powstaje pytanie, czy to rozwiązanie ma jakieś wady. Jedyną wadą stosowania funkcji i procedur jest nieznaczne spowolnienie wykonywania programu. Opóźnienie to spowodowane jest przez takie czynności jak: wywołanie podprogramu, stworzenie dla niego ramki stosu, usunięcie ramki stosu i powrót do fragmentu programu, który wywołał podprogram. W większości przypadków ten narzut czasu nie ma znaczenia i na pewno nie jest argumentem na rzecz zarzucenia używania podprogramów i powrotu do pisania programów monolitycznych (bez podziału na funkcje i procedury). 11 9. Przerwanie wykonania funkcji lub procedury Język Pascal umożliwia przerwanie wykonania funkcji lub procedury za pomocą wywołania procedury exit. Po jej wykonaniu sterowanie wraca do następnego rozkazu, znajdującego się za wywołaniem procedury lub funkcji, której wykonanie zostało przerwane. Jeśli procedura exit zostanie wywołana w programie głównym, to nastąpi jego zakończenie. W ten sam sposób działa procedura halt, z tym że ona przerywa wykonanie programu niezależnie od tego, czy zostanie wywołana w bloku głównym programu, czy w podprogramie18. Dodatkowo można ją wywołać z parametrem faktycznym będącym liczbą naturalną. Ta liczba jest kodem zakończenia programu (0 – poprawne wykonanie, wartość różna od zera – wystąpił błąd). 18 Nawet jeśli jest to zagnieżdżona procedura lub funkcja. 12