Podrozdziały


8.9 Instrukcje iteracyjne (pętle)

Instrukcje iteracyjne, zwane pętlami (ang. iterative statement, loop) mają charakter cykliczny: wykonanie instrukcji jest powtarzane dopóki spełniony jest pewien warunek logiczny. Programista powinien zapewnić, że wykonanie pętli skończy się, a więc że warunek logiczny, od którego zależy dalsze powtarzanie instrukcji, będzie kiedyś niespełniony.

Inną możliwością przerwania pętli jest zastosowanie instrukcji powrotu (return), zaniechania (break) lub skoku (goto), co omówimy dalej.

Instrukcje iteracyjne występują w trzech podstawowych odmianach, przy czym każda forma może być przekształcona do każdej z pozostałych; wybór jednej z tych form zależy tylko od wygody i upodobań programisty.

Przebieg każdej pętli może być modyfikowany lub przerwany instrukcjami breakcontinue.


8.9.1 Pętla while

Pętla while ma postać

       while ( b ) instr
gdzie wyrażenie b ma wartość logiczną a  instr jest instrukcją. Składnia wymaga jednej instrukcji instr, więc jeśli ma ich być więcej, to należy wprowadzić instrukcję grupującą (patrz rozdział o instrukcjach złożonych ), tak jak to było w przypadku instrukcji warunkowych. Wykonanie instrukcji polega na cyklicznym powtarzaniu następujących czynności: Na przykład poniższy fragment programu wyznaczy pierwszą liczbę naturalną postaci 2n, która jest większa od zadanej liczby lim:
       int liczba = 1;
       while ( liczba <= lim ) liczba *= 2;
Natomiast fragment poniższy spowoduje, że program będzie wczytywał dane od użytkownika aż do chwili, gdy poda on liczbę w zakresie [5, 100]:
       int wiek = 0;
       while ( wiek < 5 || wiek > 100) cin >> wiek;


8.9.2 Pętla do

Pętla do-while ma postać

       do instr while ( b )
gdzie wyrażenie b ma wartość logiczną, a  instr jest pewną instrukcją. Składnia wymaga jednej instrukcji instr, więc jeśli ma ich być więcej, to należy wprowadzić instrukcję grupującą (tak jak to było w przypadku instrukcji warunkowych i pętli while). Wykonanie instrukcji polega na cyklicznym powtarzaniu następujących czynności: Jak widać, instrukcja do...while różni się od instrukcji while tym, że warunek kontynuowania pętli sprawdza się po wykonaniu instrukcji instr, a nie przed. Wynika z tego w szczególności, że instrukcja instr będzie zawsze wykonana chociaż raz, nawet jeśli wartość b wynosi false (w pętli while, jeśli b ma wartość false, instrukcja instr nie byłaby wykonana ani razu).

Poniższy program symuluje serię rzutów dwiema kostkami do gry i kończy się, gdy wyrzucone zostaną dwie szóstki. Użyto tu standardowego generatora liczb losowych (rand inicjowana jednokrotnym wywołaniem funkcji srand z nagłówka <cstdlib>) do losowania wyrzucanej liczby oczek na każdej z obu kostek. Ponieważ liczba prób (rzutów) nie jest z góry znana, a zawsze trzeba i tak wykonać co najmniej jeden rzut, forma do...while stanowi tu najbardziej naturalny wybór dla realizacji pętli:


P49: kostki.cpp     Pętla do...while

      1.  #include <iostream>
      2.  #include <cstdlib>
      3.  using namespace std;
      4.  
      5.  int main() {
      6.      int x, y, roll = 0;
      7.  
      8.      srand(unsigned(time(0)));
      9.  
     10.      do {
     11.          x =  (int)(rand()/(RAND_MAX + 1.)*6)+1;
     12.          y =  (int)(rand()/(RAND_MAX + 1.)*6)+1;
     13.          cout << "Rzut nr " << ++roll   << ": ("
     14.               <<  x << ", " << y << ")" << endl;
     15.      } while (x + y != 12);
     16.  }

Przykładowy wynik tego programu:

    Rzut nr 1: (2, 1)
    Rzut nr 2: (5, 4)
    Rzut nr 3: (1, 5)
    Rzut nr 4: (2, 5)
    Rzut nr 5: (4, 5)
    Rzut nr 6: (4, 5)
    Rzut nr 7: (4, 4)
    Rzut nr 8: (5, 6)
    Rzut nr 9: (1, 6)
    Rzut nr 10: (4, 3)
    Rzut nr 11: (1, 2)
    Rzut nr 12: (2, 2)
    Rzut nr 13: (5, 2)
    Rzut nr 14: (3, 2)
    Rzut nr 15: (2, 4)
    Rzut nr 16: (1, 3)
    Rzut nr 17: (5, 6)
    Rzut nr 18: (6, 6)
Oczywiście, ponieważ używamy tu generatora liczb losowych, wyniki będą się różnić przy każdym uruchomieniu programu.


8.9.3 Pętla for

Pętla for ma postać

       for ( init ; b ; incr ) instr
gdzie instr jest instrukcją (być może złożoną, być może pustą). Wyrażenie b ma wartość logiczną; jeśli jest pominięte, to przyjmuje się wartość true. Wyrażenie init może być Część „inkrementująca”, oznaczona tu przez incr, jest listą oddzielonych przecinkami instrukcji wyrażeniowych, albo jest pominięta.

Nawet jeśli poszczególne elementy (init, b lub incr) są pominięte, nie wolno pominąć średników ani nawiasów okrągłych.

Przebieg wykonania instrukcji for jest następujący:

  1. Jeśli init nie jest pominięte, to wykonywane są instrukcje zawarte w  init. Jeśli jest to lista instrukcji wyrażeniowych, to wykonywane są w kolejności od lewej do prawej; wartości tych instrukcji są ignorowane. Jeśli jest to jedna instrukcja deklaracyjna, to zasięgiem zadeklarowanych tam zmiennych (których może być wiele, ale wszystkie tego samego typu) jest cała pętla, czyli dalsza część init, b, incrinstr. Instrukcje zawarte w  init, jeśli nie są pominięte, są wykonywane zawsze (nawet gdy instr nie będzie wykonana ani razu) i zawsze tylko raz — podczas „wejścia” do pętli.
  2. Obliczana jest wartość wyrażenia b lub przyjmowana jest wartość true, jeśli wyrażenie to jest pominięte. Jeśli wartością b jest false, wykonanie pętli kończy się. Jeśli tą wartością jest true — przechodzimy do kroku 3.
  3. Wykonywane jest ciało pętli, czyli instr.
  4. Jeśli część incr nie jest pusta, wykonywane są zawarte tam instrukcje, a wartość tego wyrażenia jest ignorowana.
  5. Powrót do kroku 2.
Należy pamiętać, że nie jest możliwe zadeklarowanie w części init zmiennych różnych typów, gdyż wymagałoby to więcej niż jednej instrukcji deklaracyjnej:
       for (double x = 0, int k = size-1; x < k; x++, k--) {
           // ... ZLE !!!
       }
Można natomiast zadeklarować więcej niż jedną zmienną tego samego typu; na przykład konstrukcja
       for (int i = 0, k = size-1; i < k; i++, k--) {
           // OK 
       }
jest prawidłowa.

Pętla for jest najczęściej stosowaną formą instrukcji iteracyjnej. Jest szczególnie wygodna, jeśli z góry wiadomo, ile będzie powtórzeń instrukcji stanowiącej ciało pętli (instrukcja instr). Zwykle w części init nadajemy odpowiednie wartości zmiennym używanym w ciele pętli, a w części incr dokonujemy ich zmiany po każdym przebiegu pętli.

Jako przykład, z wykorzystaniem instrukcji deklaracyjnej w części inicjującej init i sekwencji instrukcji wyrażeniowych w części inkrementującej incr, rozważmy poniższy program, w którym funkcja reverse odwraca kolejność elementów w tablicy liczb całkowitych:


P50: revers.cpp     Pętla for

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  void reverse(int *tab, int size) {
      5.      if ( size < 2 ) return;
      6.  
      7.      for (int i = 0, k = size-1, aux; i < k; i++, k--) { 
      8.          aux    = tab[i];
      9.          tab[i] = tab[k];
     10.          tab[k] = aux;
     11.      }
     12.  }
     13.  
     14.  void printTab(int *tab, int size) {
     15.      cout << "[ ";
     16.      for (int i = 0; i < size; i++)
     17.          cout << tab[i] << " ";
     18.      cout << "]" << endl;
     19.  }
     20.  
     21.  int main() {
     22.      int tab[] = { 1, 3, 5, 7, 2, 4, -9, 12 };
     23.      int size = sizeof(tab)/sizeof(tab[0]);
     24.  
     25.      printTab(tab,size);
     26.      reverse (tab,size);
     27.      printTab(tab,size);
     28.  }

Pętla w linii  przebiega jednocześnie po elementach tablicy od początku (zmienną indeksującą jest tu i) i od końca (elementy indeksowane zmienną k); pętla kończy się, gdy te dwa indeksy spotkają się. W ten sposób zamieniane są miejscami element pierwszy z ostatnim, drugi z przedostatnim itd. W części inicjującej pętli zadeklarowaliśmy, prócz zmiennych indeksujących, również zmienną pomocniczą aux wykorzystywaną przy zamianie wartości każdej pary elementów tablicy. Wynik tego programu:

    [ 1 3 5 7 2 4 -9 12 ]
    [ 12 -9 4 2 7 5 3 1 ]


8.9.4 Pętla foreach

W standardzie C++11 wprowadzono jeszcze jedną formę pętli, tzw. pętlę „foreach”. Może ona być stosowana do przebiegania (iterowania po) kolekcji z biblioteki standardowej (na przykład std::array), ale działa również dla tablic statycznych —  pod warunkiem, że w danym zakresie tablica jest widoczna jako posiadająca typ tablicowy (a nie wskaźnikowy, jak to ma miejsce po przesłaniu tablicy do funkcji). Składnię przedstawimy na przykładzie:


P51: foreach.cpp     Pętla „foreach”

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  int main() {
      5.      int arr[] = {1,2,3,4,9,8,7,6};
      6.  
      7.      for (int e  : arr) cout << e << " ";   
      8.      cout << endl;
      9.      for (int& e : arr) e -= 1;             
     10.      for (auto e : arr) cout << e << " ";   
     11.      cout << endl;
     12.  }

W linii zmienna e będzie w każdym przebiegu pętli zainicjowana niemodyfikowalną kopią kolejnych elementów tablicy arr. W linii natomiast, w każdym przebiegu pętli e będzie referencją do kolejnego elementu tablicy, a zatem elementy te można modyfikować (tu odejmujemy od wszystkich elementów jedynkę). Jak widzimy w linii , nie musimy specyfikować jawnie typu elementów — kompilator może ten typ wydedukować z typu tablicy (bardziej ogólnie z typu kolekcji). To samo dotyczy referencji: w linii mogliśmy napisać auto& zamiast int&. Wydruk programu to

    1 2 3 4 9 8 7 6
    0 1 2 3 8 7 6 5

T.R. Werner, 23 lutego 2019; 23:59