« poprzedni punkt  następny punkt »

2. Iteracje

Bardzo często pewne instrukcje lub grupy instrukcji będziemy chcieli wykonywać wielokrotnie.
Na przykład określoną liczbę razy. Lub dopóki spełniony jest jakiś warunek.

Zapewniają to instrukcje sterujące nazywane instrukcjami pętli iteracyjnych.
Rozważymy dwie formy takich instrukcji.

Pierwsza służy do wykonywania instrukcji określoną liczbę razy, jednocześnie zmieniając wartość zmiennej, nazywanej licznikiem. Ma ona postać:


    do licznik = wart_pocz to wart_konc [ by krok ]
        ins1;
        ins2;
        ...
        insN;
    end


gdzie:
  • licznik - nazwa zmiennej będącej licznikiem
  • wart_pocz - początkowa wartość licznika
  • wart_konc - końcowa wartość licznika
  • krok - o ile zwiększa się w każdej iteracji wartość licznika; gdy nie podano klauzuli by, przyjmuje się krok = 1
  • instrukcja1,... instrukcjaN - instrukcje wykonywane w pętli

Pętla ta wykonywana jest w następujący sposób:

Zmienne oznaczające liczniki pętli nazywamy zwykle krótko: i, j, k, l
  1. Zmiennej licznik przypisywana jest wartość wyrażenia wart_pocz
  2. Sprawdzany jest warunek: licznik <= wart_konc. Jeżeli warunek jest prawdziwy to wykonywane są instrukcje zawarte w pętli (do napotkania słowa end - a więc ins1, ins2.. insN). W przeciwnym razie (jeżeli licznik > wart_konc) sterowanie opuszcza pętle i jest przekazywane do instrukcji po słowie end.
  3. Zmienna licznik zostaje zwiększona o wartość wyrażenia krok. Wracamy do punktu 2

Rozpatrzmy przykłady. Poniższy program wypisuje na konsoli kwadraty liczb całkowitych od 1 do 100.

/* Kwadraty liczb całkowitych od 1 do 100 */

do i = 1 to 100
   say i i*i;
end

Zmiennej i nadawana jest wartość 1. Wartość ta mniejsza jest od 100, zatem wykonywana jest instrukcja w pętli (say ...), wyprowadzająca na konsolę wartość zmiennej i oraz - po spacji - jej kwadrat. Przy napotkaniu słowa end sprawdzany jest warunek kontynuacji pętli (czy i <= 100). Warunek jest prawdziwy, zatem i zwiększa się o 1 a instrukcje pętli wykonywane są znowu (wypisywana jest wartość i oraz jej kwadrat: 2 4.

Terminem sterowanie określamy kolejność wykonania instrukcji programu. Sformułowanie "sterowanie przekazywane jest do instrukcji ins " oznacza, że kolejną wykonywaną instrukcją w programie będzie ins .

I tak dalej: dopóki wartość i nie osiągnie 101. W tym momencie warunek kontynuacji pętli (i <= 100) nie jest spełniony i sterowanie przekazywane jest do instrukcji po słowie end. Ponieważ nie ma już żadnych instrukcji - program kończy działanie.


Inny przykładowy program symuluje spłaty kredytu. Podajemy na wejściu: wielkość kredytu, liczbę lat kredytu oraz jego oprocentowanie (w %). Oprocentowanie pobierane jest wraz z miesięczną ratą i obliczane jako miesięczne oprocentowanie niespłaconej części kredytu. Program wyprowadza dla kolejnych miesięcy okresu kredytowania należne spłaty, a w podsumowaniu sumę spłat i informację o ile (na skutek oprocentowania) przekroczy ona wielkość kredytu.

/* Symulacja splaty kredytu */

/*---------------------- Wprowadzanie danych */
say "Kredyt?";
kredyt = linein();
say "Liczba lat kredytu?";
lata = linein();
say "Oprocentowanie w skali rocznej?"
proc = linein();

/*---------------------- Obliczenia i wyniki */

proc = (proc/100)/12;   /* odsetki miesięczne */
lrat = lata * 12;       /* liczba rat */
rata = kredyt / lrat;   /* wielkość raty */

doSplaty = kredyt;      /* ile pozostało do spłaty */

sumaSplat = 0;          /* ile splacono: raty + oprocentowanie */

do i = 1 to lrat
   splata = rata + proc*doSplaty; /* splata = rata + nalezne odsetki */
   doSplaty = doSplaty - rata;
   sumaSplat = sumaSplat + splata;
   say i ':' splata;                 /* informacja o splacie dla i-go miesiąca */
end

say "Suma spłat   =" sumaSplat;
say "Ponad kredyt =" sumaSplat - kredyt;

W tym programie pętla do... wykonywana jest tyle razy ile wynosi liczba rat do spłacenia. Zmienna i oznacza numer raty (miesiąca spłaty). Za każdym razem wyliczana jest spłata (która wynosi wielkość raty + miesięczna wielkość odsetek od niespłacanej jeszcze części kredytu). Na konsolę wyprowadzany jest numer miesiąca oraz przypadająca na ten miesiąc spłata. Gdy i = lrat (oststni miesiąc spłaty) po raz ostatni obliczana i wyprowadzana jest informacja o spłacie. Następnie i zwiększa się o 1 i wynosi lrat +1. Warunek kontynuacji pętli nie jest spełniony, zatem sterowanie opuszcza pętlę (przekazywane jest do instrukcji po słowie kluczowym end - czyli say "Suma splat..."). Na konsolę wyprowadzane są informacje wyjściowe i program kończy działanie.

Wydruk działania programu:

Kredyt?
10000
Liczba lat kredytu?
1
Oprocentowanie w skali rocznej?
20
1 : 1000.00000
2 : 986.111111
3 : 972.222222
4 : 958.333333
5 : 944.444445
6 : 930.555556
7 : 916.666667
8 : 902.777778
9 : 888.888889
10 : 875.000000
11 : 861.111111
12 : 847.222223
Suma spłat = 11083.3333
Ponad kredyt = 1083.3333

Zwróćmy uwagę na zastosowany w tym programie sposób sumowania spłat. Sumę spłat uzyskamy w zmiennej sumaSplat. Na początku jej wartość równa jest 0. W pierwszej iteracji do sumaSplat dodawana jest wartośc pierwszej spłaty (w tej chwili sumaSplat równa jest pierwszej spłacie). W każdej następnej iteracji poprzednia suma spłat jest zwiększana o bieżącą spłatę, a wynik znowu zapisywany w zmiennej sumaSplat.
Jest to powszechny sposób sumowania (akumulowania) różnych wartości. Może on być zastosowany nie tylko do wartości numerycznych, ale również w innych przypadkach, np. do kumulatywnego łączenia łańcuchów znakowych.

Np. w poniższym programiku wypisujemy napis składający się z n-razy kolejno łączonych słów "Ala" i "kot".

/* Łączenie napisów w pętli */

word1 = "Ala";
word2 = "kot";

n = 8;
string = "";

do i = 1 to n
  if ( i // 2 = 0 ) then string = string||word2;
  else string = string||word1;
end

say string;

Wydruk działania programu
AlakotAlakotAlakotAlakot

Na początku zapisujemy do zmiennej string pusty łańcuch znakowy (""). Następnie w pętli na przemian dodajemy do bieżącej zawartości zmiennej string albo słowo word1, albo słowo word2.

Sposób naprzemiennego dodawania jest następujący: jeśli numer iteracji (wartość zmiennej i) jest parzysty, dodajemy zmienną word2, a jeśli jest nieparzysty - zmienną word1.

Uwaga! W Javie operatorem reszty z dzielenia jest %



Parzystość sprawdzamy za pomocą użycia operatora reszty z dzielenia (który w REXXie oznaczany jest //). Reszta z dzielenia przez 2 dla liczb parzystych równa jest 0, a dla nieparzystych ma inną wartość niż 0. Zwróćmy też uwagę, że wykorzystaliśmy priorytety operatorów: priorytety operatorów arytmetycznych (w tym: reszty z dzielania) są wyższe niż priorytet operatora równości =, dlatego w wyrażenie i // 2 = 0 najpierw jest stosowany operator //, a później dopiero operator =,  porównujący wynik wyrażenia i // 2 z zerem.

Proszę zapisać i uruchomić omawiane wyżej programy na własnym komputerze.

Drugą, przedstawianą tu, formę pętli iteracyjnej realizuje instrukcja do while

Ma ona postać:

        do while ( warunek )
            ins1;
            ins2;
            ...
            insN;
        end

Instrukcje w pętli (ins1, ins2, ... insN) wykonywane są o ile i dopóki warunek jest prawdziwy.

Poniższy program służy do testowania działania pętli do while.

say "Podaj a: ";
a = linein();
say "Podaj b: ";
b = linein();
say "a = " a  "b = " b;

n = 0
do while ( a > b)
   a = a - 1;
   n = n + 1
end

say "Pętla została wykonana: " n "razy."
say "a = " a  "b = " b;

Przykładowy wydruk działania programu
Podaj a:
1078.1
Podaj b:
23.5
a = 1078.1 b = 23.5
Pętla zostala wykonana: 1055 razy.
a = 23.1 b = 23.5

Podajemy w nim wartości zmiennych a i b. Pętla do while zawiera warunek (a > b) zatem:
  • jej wykonanie rozpocznie się tylko wtedy, gdy warunek jest spełniony (a > b)
  • w pętli a ulegnie zmniejszeniu o 1
  • jeśli nadal będzie większe od b, to instrukcje petli zostaną powtórzone
  • będą one powtarzane dopóki a będzie większe od b
  • gdy a stanie się równe lub mniejsze od b, pętla zakończy działanie

Proszę zapisać i uruchomić omawiany wyżej program na własnym komputerze.

Pętle iteracyjne (do..., do while) jak również instrukcje if  mogą być dowolnie zagnieżdżone.

Zagnieżdżenie konstrukcji językowych oznacza umieszczanie ich jedna - w - drugiej

Na przykład, całkiem dopuszczalny jest taki schemat:

do while (a > b)
  do i = 1 to N
    ....
    do j = 1 to M
       .....
       do while (s = "")
          ...
       end
    end
  end
end

Istotną sprawą przy zagnieżdżaniu instrukcji sterujących jest "dopasowanie" ich początków i końców (w naszym przypadku każdemu otwierającemu do musi odpowiadać zamykające end).
Przy takim dopasowaniu pomaga nam odpowiednie formatowanie kodu programu (wcinanie wierszy).
W powyższym przykładzie widac jeden ze sposobów wykonywania "wcięć".

Wcięcia w kodzie programu zwiększają jego czytelność i pozwalają łatwiej odnajdywać błędy.
Wcięcia są jednym z elementów tzw. stylu programowania, czyli sposobu formatowania kodu tak by był przejrzysty.

Należy jednak pamiętać, że sposób formatowania kodu programu nie ma nic wspólnego z jego poprawnością składniową i semantyką. Żaden kompilator i żaden interpreter nigdy nie stara się odczytać intencji programisty na podstawie zastosowanych wcięć.
Np. jeżeli napiszemy:

if  (a > b) then  
        if ( c > d) then  x = y;
else x = z;

to być może - w intencji, dającej się odczytać z wcięcia else (podpisanie go pod pierwszym if) - miało to oznaczać wykonanie instrukcji else gdy warunek (a > b) jest niespełniony.
Oczywiście tak się wcale nie stanie i else dotyczyć będzie drugiego if.

Wcięcia nie mają nic wspólnego z semantyką programu.

Możemy zmieniać naturalne działanie pętli iteracyjnych za pomocą instrukcji:

  • przerwania działania pętli
  • kontynuacji działania pętli
W Javie służy temu instrukcja break

Przerwanie działania pętli uzyskujemy w REXXie za pomocą instrukcji leave (opuść pętlę). Przerywana jest najbardziej wewnętrzna pętla, lub jeśli w instrukcji leave użyjemy etykiety (leave jakasEtykieta) - pętla oznaczona tą etykietą.

Np.

do i = 1 to 100000
   n = i * i;
   if (n > 20000) then leave;
end
say i n;
Wynik
142 20164

Pętla wykonała się 142 razy (a nie 100000 jak wskazuje górna wartość licznika), ponieważ gdy n jest większe od 20000 przerywamy działanie pętli za pomocą instrukcji leave (sterowanie zostaje przekazane poza end kończące pętlę).

Kontynuacja działania pętli polega na natychmiastowym przekazaniu sterowania na koniec pętli, co inicjuje nową iterację.

W Javie mamy tu instrukcję continue


W Rexxie służy temu instrukcja iterate (tak samo jak leave może dotyczyć najbardziej wewnętrznej z zagnieżdżonych pętli lub pętli oznaczonej podaną etykietą).

Na przykład w poniższym programie wczytujemy z konsoli wszystkie wprowadzone przez użytkownika napisy, pomijając puste wiersze (np. spacje lub samo ENTER), łączymy je w jeden łańcuch znakowy i wyprowadzamy po wprowadzeniu przez użytkownika słowa quit, kończąc na tym działanie programu.

out = '';

do forever
  txt = linein();
  if ( txt = "" ) then iterate;
  if ( txt = "quit" ) then leave;
  out = out txt;
end
say out;

W Javie taką nieskończoną pętlę możemy zapisać jako while ( true ) {...}

Warto tu zwrócić uwagę na składniową konstrukcję do forever, oznaczającą pętlę nieskończoną (wykonuje się wiecznie; oczywiście zawsze trzeba mieć jakiś sposób, warunek przerwania takiej pętli).


Generalnie, warto szczególnie podkreślić problem właściwego kończenia wykonywania pętli iteracyjnych. Niezwykle często bowiem na skutek błędów w programie pętle nie kończą działania i program wykonuje się bez końca. Czasami jedynym sposobem jego zatrzymania jest zakończenie procesu z poziomu systemu operacyjnego.


« poprzedni punkt  następny punkt »