Instrukcje warunkowe i iteracyjne.
Transkrypt
Instrukcje warunkowe i iteracyjne.
Podstawy Programowania Wykład trzeci: Instrukcje warunkowe i iteracyjne. 1. Instrukcja warunkowa Jeśli w programie wykonanie określonych instrukcji jest zależne od pewnych warunków to możemy skorzystać z jednej z dwóch istniejących w Pascalu konstrukcji językowych. Pierwszą z nich jest instrukcja warunkowa. Oto jej schemat ogólny: if warunek then instrukcja1 else instrukcja2; Ten zapis możemy przeczytać jako: „jeśli warunek jest prawdziwy, to wykonaj instrukcja1, w przeciwnym przypadku wykonaj instrukcja2”. Warunek może być zmienną typu boolean lub wyrażeniem, którego wartość ma taki właśnie typ. Za pomocą schematu blokowego można przedstawić działanie instrukcji warun kowej następująco: Start Czy warunek NIE prawdziwy? TAK instrukcja1 instrukcja2 Stop Możliwe jest też pominięcie członu else tej instrukcji, wówczas ma ona postać: if warunek then instrukcja; 2 a jej działanie można opisać następującym schematem blokowym: Start Czy warunek NIE prawdziwy? TAK instrukcja Stop Zamiast złożonych instrukcji warunkowych, np.: if y<0 then if z>0 then if x<>0 then a:=(y+2)/x else else else a:=1; można stosować prostszy zapis łącząc warunki za pomocą odpowiednich opera torów: if (y<0) and (z>0) and (x<>0) then a:=(y+2)/x; if y>=0 then a:=1; 3. Instrukcja wielokrotnego wyboru Jeśli w programie istnieje kilka grup instrukcji, których wykonanie zależy od kilku wartości jednej zmiennej lub jednego wyrażenia, to możemy użyć instrukcji wielokrotnego wyboru, zamiast stosować instrukcję if. Ta instrukcja ma następującą postać ogólną: 3 case selektor of wartość_1 : instrukcja_1; wartość_2 : instrukcja_2; wartość_3 : instrukcja_3; . . . wartość_n : instrukcja_n; else instrukcja; end; Wartości w tym wyrażeniu muszą mieścić się typie selektora. Zamiast pojedynczej wartości możemy przy każdym przypadku umieścić ich grupę, rozdzielając je przecinkami lub podać ich zakres, np.: 0..5. Człon else można pominąć. Selektor jest zmienną typu porządkowego lub wyrażeniem mającym wartość tego typu. Oto schemat blokowy dla tej instrukcji: Start Czy TAK selektor=wartość_1 ? instrukcja_1 NIE Czy TAK selektor=wartość_2 ? instrukcja_2 NIE instrukcja Stop 4 4. Instrukcje iteracyjne (pętle) Jeśli zachodzi konieczność kilkukrotnego wykonania instrukcji, to w języku Pascal możemy uczynić to za pomocą trzech dostępnych instrukcji iteracyjnych (pętli). Pierwsza z nich, pętla for, występuje w dwóch postaciach i pozwala powtórzyć instrukcję ustaloną liczbę razy. Oto schematy dla obu postaci tej instrukcji: for licznik:=wp to wk do instrukcja; for licznik:=wp downto wk do instrukcja; Zmienna licznik nazywana jest licznikiem lub zmienną sterującą pętli. Musi ona być typu porządkowego. W przypadku pierwszej postaci pętli wartość po czątkowa (wp) musi być mniejsza lub równa wartości końcowej (wk). W przy padku drugiej postaci musi być większa lub równa. Oto schemat blokowy dla pierwszej postaci pętli for: Start Czy wp<=wk ? TAK licznik:=wp NIE instrukcja TAK licznik=wk ? NIE licznik:=licznik+1 instrukcja Stop Jeśli liczba powtórzeń pętli będzie zależna od spełnienia określonego warunku, to należy zastosować pętlę while lub repeat. Oto ogólne postaci tych pętli: while warunek do instrukcja; 5 repeat instrukcja until warunek; Zapis pętli while możemy odczytać następująco: dopóki spełniony jest warunek wykonuj instrukcję, natomiast pętlę repeat następująco: powtarzaj instrukcję, dopóki warunek nie jest spełniony. Oto schematy blokowe dla obu instrukcji: Start instrukcja warunek ? TAK NIE Stop Start instrukcja NIE warunek ? TAK Stop Warunek w przypadku obu rodzajów pętli musi spełniać takie same wy magania, jak w przypadku instrukcji warunkowej. Wewnątrz pętli musimy umieścić instrukcję, które zagwarantują, że warunek dla pętli while przestanie kiedyś być prawdziwy, a dla pętli repeat stanie się kiedyś prawdziwy. Wszystkie pętle można zagnieżdżać, tzn. wewnątrz pętli umieszczać inną pętlę, nie koniecznie tego samego typu. 4. Instrukcje continue i break Instrukcji continue i break w języku Pascal używamy wyłącznie w połączeniu z instrukcjami iteracyjnymi. Umieszczamy je wewnątrz pętli, ale nie występują 6 w nich samodzielnie – są częścią instrukcji warunkowych. Jeśli warunek jest spełniony, to instrukcja break przerywa wykonanie pętli, natomiast instrukcja continue przerywa wykonanie bieżącej iteracji (powtórzenia) pętli i przechodzi do następnej. 5. Instrukcje strukturalne (złożone) Jeśli chcemy zgrupować kilka instrukcji, tak aby były one traktowane jako jed na, to musimy w tym celu użyć nawiasów syntaktycznych begin i end. Instruk cje, które mają być zgrupowane razem umieszczamy między tymi słowami kluczowymi. Taką konstrukcję nazywamy instrukcją strukturalną lub złożoną. W przypadku instrukcji warunkowych i wielokrotnego wyboru może ona nam posłużyć do objęcia warunkami większej liczby pojedynczych instrukcji. W przypadku pętli for i while umożliwia wielokrotne wykonanie grupy instrukcji. Pętla repeat nie wymaga stosowania instrukcji strukturalnej. Uwaga: po ostatniej instrukcji znajdującej się między begin i end nie jest wymagane umieszczanie średnika, ale dobrym zwyczajem jest nie stosowanie się do tej zasady. Po słowie end w większości przypadków umieszczamy średnik. Wyjątkiem od tej reguły jest blok główny programu, który również jest instrukcją złożoną. W jego przypadku po słowie kluczowym end występuje zawsze kropka. 6. Instrukcja goto Instrukcja goto może być użyta jako zamiennik opisywanych wcześniej pętli. Wymaga ona zadeklarowania i umieszczenia w programie etykiety określającej punkt do którego ma przejść sterowanie po wykonaniu instrukcji goto. Ponie waż słowo kluczowe goto występuje w większości popularnych języków programowania było ono nadużywane przez programistów, co prowadziło do powstawania trudnych do analizy kodów źródłowych programów. Uwagę na to zjawisko zwrócił wybitny informatyk teoretyczny Edsger Dijkstra, który w jednym ze swych artykułów wręcz zakazał używania w programach tej instrukcji. Obecnie jest ona używana bardzo rzadko. Stosuje się ją najczęściej do obsługi błędów, lub wtedy, kiedy trzeba przyspieszyć działanie programów (optymalizacja czasu wykonania programu). 7. Przykłady Pierwszy przykład ilustruje użycie niesławnej instrukcji goto1 oraz instrukcji 1 Zgodnie z zaleceniami E. Dijkstry, proszę jej nie wykorzystywać w swoich programach, chyba że otrzymacie Państwo wprost polecenie jej użycia. 7 continue. Zadaniem programu jest wypisanie liczb naturalnych od 1 do 15 na ekran, a następnie wypisanie wszystkich liczb naturalnych parzystych od 2 do 20 w kolejnych wierszach. program skok_i_kontynuacja; uses crt; label ety1; var i:byte; begin ety1: i:=i+1; writeln(i); if i<>15 then goto ety1; readln; i:=0; {Wypisz wszystkie parzyste.} while i<>20 do begin i:=i+1; if odd(i) then continue; writeln(i); end; readln; end. Pierwsza czynność jest zrealizowana za pomocą instrukcji goto. Wewnątrz „pętli” wartość zmiennej i jest zwiększana o jeden (można zamiast zastosowanej tu instrukcji przypisania użyć procedury inc) i wypisywana na ekran. W instrukcji warunkowej następuje sprawdzenie, czy wartość i jest różna od 15. Jeśli nie, to wykonywany jest skok do instrukcji oznaczonej etykietą ety1, jeśli tak, to program wykonuje następną w kolejności instrukcję, czyli readln. Wypisanie liczby parzystych realizowane jest za pomocą instrukcji while. Tu również wykorzystywana jest zmienna i, której przed wykonaniem pętli ponownie jest nadawana wartość 0. Tym razem w instrukcji warunkowej badamy, czy jej wartość jest nieparzysta, jeśli tak, to wracamy na początek pętli, pomijając instrukcję writeln(i). Proszę zwrócić uwagę na zastosowanie instrukcji strukturalnej w tej pętli. Jeśli nie użylibyśmy słów kluczowych begin i end, to wykonywała by się tylko instrukcja i:=i+1;2 Proszę również zwrócić 2 Mała dygresja: Przy opisie pętli while i repeat wspomniane było, że należy umieścić w nich in strukcje, które zapewnią spełnienie warunku ich zakończenia. Opisywana instrukcja pełni między innymi taką właśnie rolę, gdyż zmienia wartość zmiennej i od której zależne jest za 8 uwagę na sformatowanie kodu źródłowego. Wszystkie instrukcje, które zamknięte są w instrukcji strukturalnej, zapisane są z wcięciem wynoszącym dwie spacje. Również w przypadku fragmentów programu zamkniętych w sekcjach typu var, const lub instrukcjach warunkowych i wielokrotnego wyboru zaleca się stosowanie takich wcięć. Nie są one wymagane przez kompilator, ale poprawiają czytelność kodu. Drugi przykładowy program oblicza największy wspólny dzielnik według al gorytmu Euklidesa, który został podany na pierwszym wykładzie. program NWD; uses crt; var M,N,R:word; begin clrscr; writeln('Podaj dwie liczby naturalne, większe od zera.'); readln(N,M); repeat R:=M mod N; if R=0 then writeln('NWD = ',N); M:=N; N:=R; until R=0; readln; end. Analizując ten program można zauważyć, że nie jest on dokładną implementa cją wspomnianego algorytmu, ponieważ kiedy R osiągnie wartość zero nadal zostaną wykonane dwie operacje przypisania. Kolejny program pokazuje, jak w prosty sposób zabezpieczyć program przed błędem użytkownika polegającym na wprowadzeniu ciągu znaków nie będących liczbą, kiedy program wymagał właśnie liczby3. Wykorzystana jest do tego celu procedura val, która konwertuje ciągu znaków na liczbę. Procedura ta wykonywana jest w pętli repeat do momentu, aż użytkownik poda ciąg znaków będący liczbą i konwersja się powiedzie (zmienna kod będzie wówczas miała wartość 0). Opisane rozwiązanie jest częścią zagadnienia nazywanego weryfikacją danych wejściowych. Każdy dobrze napisany program powinien kończenie pętli. Instrukcja ta jest zbędna w przypadku pętli for, dla której inkrementacja licznika jest wykonywana automatycznie. 3 Czyli przed podaniem złych danych na wejście programu. 9 zawierać kod sprawdzający poprawność wprowadzanych danych. program weryfikacja; var i,kod:integer; nap:string; begin repeat readln(nap); val(nap,i,kod); until kod=0; write(i); readln; end. Powyższy program można uzupełnić o instrukcje informujące użytkownika o wyniku konwersji i instruujące go co robić gdy się ona nie powiedzie. Następny przykład to program liczący silnię. Aby zrozumieć jego działanie naj pierw przypomnijmy sobie definicję działania „silnia”: 0! = 1 1! = 1 n! = 1 * 2 * 3* ... * (n1)*n Wypiszmy kilka przykładów silni dla małych n: 1! = 1 2! = 1*2 3! = 1*2*3 4! = 1*2*3*4 czyli 2! = 1!*2 3! = 2!*3 4! = 3!*4 Z powyższej tabeli wynika, że jeśli znamy wartość (n1)!, to aby policzyć n! na 10 leży pomnożyć tę wartość przez n. To spostrzeżenie wykorzystywane jest w programie liczącym silnie: program silnia; uses crt; var sil:word; i,n:byte; begin sil:=1; repeat writeln('Podaj liczbę naturalną należącą do przedziału [0,8]'); readln(n); until n<=8; for i:=1 to n do sil:=sil*i; writeln('Wartość silni wynosi: ',sil); readln; end. W zmiennej n program będzie przechowywał wartość dla której należy obliczyć silnię. Wartość ta pobierana jest od użytkownika za pomocą procedury readln. Nie powinna ona być mniejsza niż 0 i większa niż 8, dlatego procedura readln jest umieszczona w pętli repeat, która sprawdza, czy użytkownik podał właściwą wartość. Wyznaczenie silni odbywa się w pętli for. Częściowe wyniki są przechowywane w zmiennej sil, która wcześniej musi być zainicjalizowana wartością 1. Prześledźmy wykonanie tej pętli dla n=3. Wykona się ona trzy ra zy. Za pierwszym razem w zmiennej sil znajdzie się wartość iloczynu 1*1, po drugim wykonaniu znajdzie się tam wartość iloczynu 1*2, a za trzecim 2*3. Na leży zauważyć, że wartość zmiennej i (licznika pętli) jest automatycznie zwięk szana o 1 po każdym wykonaniu pętli. Ograniczenie wartości zmiennej n wiąże się z typem zmiennej sil, w której przechowywany są wyniki częściowe i wynik ostateczny. Dla n>8 następuje przepełnienie, czyli otrzymywane wartości nie mieszczą się w typie word. Zagnieżdżanie pętli jest ilustrowane programem wyznaczającym kolejne liczby pierwsze. Najprostszy sposób na znalezienie wszystkich liczb pierwszych z okre ślonego przedziału polega na zbadaniu, czy każda z nich dzieli się przez liczby od niej mniejsze (oprócz oczywiście zera i jedynki). Oto kod programu, który działa według takiego algorytmu. 11 program primes; uses crt; var i,j:word; prime:boolean; begin clrscr; for i:=2 to high(word) do begin prime:=true; for j:=2 to i1 do if i mod j = 0 then begin prime:=false; break; end; if prime then write(i,', '); end; readln; end. Zewnętrzna pętla for, pełni rolę „generatora” liczb z przedziału [2, 65535], które mają podlegać sprawdzeniu. Te liczby są wartościami zmiennej sterującej i. Sprawdzenie liczby dokonywane jest w pętli wewnętrznej. Przed jej rozpoczęciem zakładamy, że testowana wartość jest liczbą pierwszą, co sprowadza się do nadania zmiennej prime, wartości true. Ta zmienna pełni rolę znacznika, określającego, czy badana liczba jest pierwsza. W wewnętrznej pętli for sprawdzamy prawdziwość naszego założenia, czyli dzielimy sprawdzaną liczbę, przez wszystkie które ją poprzedzają. Jeśli liczba nie jest pierwsza, to podzieli się bez reszty przez jedną z nich4. Jeśli zajdzie taka sytuacja to nadajemy zmiennej prime wartość false i przerywamy wykonanie pętli wewnętrznej instrukcją brake. Jeśli badana liczba jest pierwsza, to pętla nigdy nie zostanie przerwana przez instrukcję break i zmienna prime nie zmieni swej wartości. Po zakończeniu pętli wewnętrznej w instrukcji warunkowej badamy, czy prime ma wartość true jeśli tak, to wypisujemy badaną liczbę na ekran, bo jest ona liczbą pierwszą. Znając konstrukcję pętli for oraz operatory bitowe możemy w prosty sposób znaleźć reprezentację binarną liczby dziesiętnej zapisanej w zmiennej określo nego typu5. Oto kod programu: 4 Inaczej: reszta z dzielenia będzie równa zero. 5 Inaczej: przedstawić liczbę dziesiętną w kodzie dwójkowym. 12 program dec2bin; uses crt; const j=32768; var x:word; i:byte; begin clrscr; writeln('Podaj liczbę naturalną.'); readln(x); writeln('Reprezentacja tej liczby w kodzie binarnym to:'); for i:=0 to 8*sizeof(x)1 do if x and (j shr i) = 0 then write('0') else write('1'); readln; end. Zakładamy, że liczba którą będziemy chcieli przeliczyć jest przechowywana w zmiennej typu word. Zamiana jej reprezentacji na binarną opiera się na fakcie, że w pamięci komputera jest ona przechowywana właśnie w postaci dwójkowej. „Przeliczenie” odbywa się w pętli for. Zmienna i będąca licznikiem pętli przyjmuje wartości od 0 do 15 (taką wartość da wyrażenie 8*sizeof(x)1). Stała j ma wartość 32768, co odpowiada liczbie binarnej, której najstarszy bit jest jedynką, a pozostałe mają wartość zero. Ten pojedynczy ustawiony bit w stałej j jest przesuwany w prawo (za pomocą operatora shr) o tyle miejsc, ile wskazuje wartość zmiennej i, co daje efekt jego przesuwania o jedną pozycję w każdej iteracji. Otrzymana wartość jest używana następnie jako argument operatora and. Drugim argumentem tego operatora jest wartość ze zmiennej x. Jeśli ten sam bit w zmiennej x, co bit o wartości jeden w wyniku wyrażenia i shr j ma wartość jeden, to operator and zwróci wartość różną od zera, jeśli nie zwróci wartość zero. To pozwala nam wypisać wartości poszczególnych bitów na ekran. Zasadę działania programu można pokazać na liczbach czterobitowych: 13 Liczba „przeliczana” ma postać dwójkową 0101, czyli 5 dziesiętnie: 0101 and 1000 =0 wypisujemy „0” 0101 and 0100 <> 0 wypisujemy „1” 0101 and 0010 = 0 wypisujemy „0” 0101 and 0001 <> 0 wypisujemy „1” Należy zauważyć, że tę metodę można również zastosować do liczb zapisanych w kodzie U2, czyli przechowywanych w zmiennych typu integer i longint6. Następny przykład liczy pierwiastki równania kwadratowego. Okazuje się, że wzory znane ze szkoły średniej nie dają poprawnych wyników w implementacji komputerowej, jeśli a⋅c ≪b , ze względu na wady typów zmiennoprzecin kowych. Aby uzyskać prawidłowe wartości należy użyć następujących wzorów: −1 q c q≡ ⋅[b sgn b ⋅ ] , x 1 = , x2= , gdzie sgn jest funkcją mającą 2 a q wartość 1 dla b>=0 i 1 dla b<0. Oto kod programu, który liczy pierwiastki równania kwadratowego, według podanych wzorów. program rownanie_kwadratowe; uses crt; var a,b,c,q,delta:real; begin clrscr; writeln('Podaj współczynniki równania kwadratowego:'); writeln('Współczynnik a musi być różny od zera!'); write('a:'); readln(a); write('b:'); readln(b); write('c:'); readln(c); delta:=sqr(b)4*a*c; if delta >= 0 then begin if b<0 then q:=0.5*(bsqrt(delta)) else q:=0.5*(b+sqrt(delta)); writeln('x1 = ',q/a:7:4,' x2 = ',c/q:7:4); end; readln; end. Ostatnie przykłady dotyczą wyliczania wartości takich funkcji matematycznych, 6 Istnieją dwie stałe MaxInteger i MaxLongInt, które są równe największym wartościom jakie można przechować w zmiennych o typach odpowiednio integer i longint. Nie mogą one jednak pełnić roli stałej j w powyższym programie. W przypadku tych typów tę rolę muszą pełnić stałe o najmniejszych wartościach. To ze względu na stosowanie kodu U2. 14 takich jak cos i exp. Istnieją oczywiście w Pascalu gotowe funkcje liczące wartości kosinusa i ex, ale warto wiedzieć jak sobie poradzić, gdyby nie były one dostępne. Wartości tych funkcji można obliczyć z następujących szeregów: x x2 x3 xk e x =1 ... ... 1! 2! 3! k! 2 4 6 x x x x 2 ⋅k cosx =1 − − ...−1k⋅ ... 2! 4 ! 6 ! 2⋅k ! Pierwszy z prezentowanych poniżej programów oblicza wartość kosinusa dla kąta /3. (kąt podajemy w radianach!): program cosinus; uses crt; const eps=1E11; var x,e,n:real; i:integer; begin clrscr; x:=pi/3; e:=1; n:=1; i:=1; while (abs(cos(x)e)) > eps do begin n:=n*(1*x*x/((2*i1)*(2*i))); e:=e+n; inc(i); end; writeln(cos(x):10:10); writeln(e:10:10); readln; end. Zanim zaczniemy analizę tego programu należy zauważyć, że każdy kolejny element szeregu otrzymujemy mnożąc jego poprzednika przez wyrażenie x2 − , gdzie i jest numerem pozycji elementu w szeregu7. 2 ⋅i −1⋅2 ⋅i W programie zmienna x ma nadaną wartość kąta, dla którego chcemy obliczyć kosinus, a zmienne e (przechowuje sumę elementów), n (wartość kolejnych elementów ciągu) i i (pozycja elementu w ciągu) są zainicjalizowane wartością 1. Obliczenia są dokonywane w pętli while, która kończy się, kiedy uzyskany przez program wynik różni się co do wartości bezwzględnej o stałą eps od wartości uzyskanej z funkcji cos8. W wierszu n:=n*(1*x*x/((2*i1)*(2*i))); liczone są wartości kolejnych elementów szeregu, a w następnym wierszu są one 7 Aby się o tym przekonać wystarczy podzielić przez siebie dwa sąsiednie elementy, w kolejności „prawy przez lewy”. 8 Jest to podstawowa zasada porównywania dwóch liczb zmiennopozycyjnych. Nie możemy ich porównać bezpośrednio, bo nigdy nie znamy ich dokładnego rozwinięcia. Wyliczamy więc ich różnicę i sprawdzamy, czy jest mniejsza od ustalonej, bardzo małej liczby. 15 dodawane do wyniku końcowego . Podobnie napisany jest program liczący wartość eksponenty. W tym przypadku x wartości kolejnych elementów ciągu różnią się o czynnik . Program liczy i wartość e24. Oto jego kod źródłowy: {$N+} program exponenta; uses crt; const eps=1E11; var e,x,n:extended; i:word; begin clrscr; e:=1; n:=1; x:=24; i:=1; while (abs(exp(x)e)) > eps do begin n:=n*(x/i); e:=e+n; inc(i); end; writeln('Funkcja exp: ',exp(x):10:10); writeln('Exp z szeregu: ',e:10:10); readln; end. Proszę zwrócić uwagę, na zastosowanie dyrektywy kompilatora {$N+}, która nakazuje mu użycie rozkazów koprocesora matematycznego. Dzięki temu możemy korzystać z typu extended. Program działa również dla innych wartości x, ale nie wszystkie one prowadzą do poprawnych wyników. Funkcja cos prawdopodobnie nie wyznacza wartości z szeregu, gdyż jest to czasochłonne. W przypadku kosinusa lepiej jest umieścić w tablicy wartości tej funkcji dla „typowych” wartości kąta. Jeśli użytkownik zażyczy sobie wartości kosinusa, której nie ma w tablicy, to można ją wyliczyć za pomocą kosinusa sumy lub różnicy kątów. Opisane metody wyznaczania wartości funkcji można również zastosować do funkcji sinus. Poniżej, dla zainteresowanych podaję wzór na szereg Taylora i Mclaurina, z których zostały wyprowadzone szeregi dla kosinusa i eksponenty: ∞ f ' ' x 0 f i x 0 2 szereg Tylora : f x =f x 0 f ' x 0 ⋅ x −x 0 ⋅x −x 0 ...=∑ ⋅ x −x 0 i 2! i! i=0 16 ∞ szereg Mclaurina : f x =f 0f ' 0⋅x f ' ' 0 2 0 ⋅x ...=∑ f i ⋅x i 2! i! i =0 (Szereg Mclaurina uzyskujemy z szeregu Taylora podstawiając za x0 wartość zero.) 17