Trudno wyobrazić sobie program mający 1753 linie kodu podobne do poniższego przykładu:
int a=20; printf("Wartość a=%d",a); int a=21; printf("Wartość a=%d",a); int a=22; printf("Wartość a=%d",a); int a=23; printf("Wartość a=%d",a); int a=24; printf("Wartość a=%d",a); int a=25; printf("Wartość a=%d",a); /// itd...
(Chociaż znam studentów… którzy w podobny sposób rozwiązują zadanie na kolokwium)
Od razu widać w kodzie pewną prawidłowość, która sprawia, że można podzielić ten kod na bloki w zasadzie identyczne, różniące się jedynie przyrostową wartością zmiennej a. Od razu “prosi się” to o zastanowienie czy istnieje mechanizm ułatwiający takie operacje i optymalizujący kod. Tym mechanizmem są pętle powodujące, że pewien fragment kodu jest powtarzany, a warunek wejścia i wyjścia z pętli gwarantują nam odpowiednią iterację (powtarzanie).
W języku C/C++ jest wyróżnionych kilka rodzajów pętli, z czego najbardziej popularne są 3 i są to:
- for ( punkt startu; warunek stopu; krok iteracji licznika… ) { …kod programu… }
- while ( …warunek… ) { …kod programu… }
- do { …kod programu… } while ( …warunek… );
Jako ciekawostkę można przytoczyć dodatkowe rodzaje pętli w postaci starej konstrukcji goto czy rekurencję. Konstrukcja goto z uwagi na wiele wad, chociażby takich wynikających z nieczytelności kodu, możliwości przekraczania granic wzajemnego zagnieżdżania prowadząc do trudno wykrywalnych błędów w konstrukcji programu, nieczytelnych warunków wyjścia jest ona mocno niezalecana i w zasadzie jej użycie jest uznawane za indolencję programisty. Użycie takiej pętli może wyglądać jak na poniższym przykładzie:
int a=0; lab: printf("Wartość a=%d\n\r",a); if (++a<10) goto lab;
Działanie tej pętli wynika z badania warunku wartości zmiennej a, która to została zadeklarowana jako int i zainicjalizowana wartością początkową równą 0. W kodzie widać etykietę o nazwie lab 1Ciąg znakowy zakończony znakiem : jest uznawany jako etykieta. Można w ten sposób oznaczać ważne punkty w programie.. Następną linią programu jest jedna z komend z naszego pierwszego przykładu – wyświetlająca wartość zmiennej a. Kolejna linia jest warunkiem badającym wartość zmiennej a i jeśli jest ona poniżej wartości granicznej (tu równej 10) następuje uruchomienie komendy po if i skok bezwarunkowy (goto) do miejsca programu oznakowanego etykietą lab. Dodatkowo w warunku znajduje się iteracja ++a, co oznacza, że wpierw wartość a zostanie zinkrementowana o 1 (pre-inkrementacja), a następnie pobrana do sprawdzenia warunku. Jak łatwo obliczyć spowoduje to 10 powtórzeń pętli, co powinno przynieś następujący skutek:
Właściwymi konstrukcjami pętli są zatem wymienione wcześniej 3 konstrukcje, i tak więc:
Spis treści
for ( ; ; ) {};
Pętla typu for jest dedykowana rozwiązaniom, kiedy znamy lub spodziewamy się konkretnej liczby powtórzeń programu – w stylu: “wykonaj coś 10 razy”. Jej konstrukcja i użycie wygląda jak w poniższym przykładzie:
for (int a=0; a<10; a++) printf("Wartość a=%d\n\r",a);
Wynikiem jest praktycznie identyczne zachowanie do zaprezentowanego powyżej.
Punkt startu określa tzw. iterator pętli, czyli zmienna dowolnego typu arytmetycznego2Typ arytmetyczny to każdy z typów o którym można powiedzieć, że ma ściśle określoną listę wartości, jaką można pogrupować według kryterium np. wzajemnego stosunku większa/mniejsza. Takim typem może być int bo wiemy, że element zbioru liczb typu int o wartości 6 jest większy od 5 itp. To samo jest w przypadku typu char analizując go względem wartości kodów znaków, o zadanej wartości, której zmiany wartości będą badane, tu: int a=0.
Warunek wyjścia to wyrażenie logiczne, które jeśli jego wynikiem jest true powoduje kontynuację działania pętli, jeśli wartość wyrażenia staje się false to powtórzenie kodu z pętli zostaje przerwane wraz z końcem bloku rozkazów (mowa o jednym rozkazie, lub bloku rozkazów ograniczonym klamrami { } ), tu mamy pojedynczą komendę printf() stąd nie ma potrzeby otaczać ją { }. Warunek ten jest sprawdzany na wejściu do ewaluacji bloku rozkazów co oznacza, że za każdym razem musi być spełniony aby blok się wykonał. Jeśli punkt wejścia jest niezgodny z warunkiem kontynuacji pętli, to blok rozkazów pętli for nie wykona się ani razu.
Krok iteracji określa w jaki sposób zmienia się wartość iteratora, tu iterator jest post-inkrementowany o 13W tym przypadku nie ma w zasadzie znaczenia czy jest to pre-, czy post-inkrementacja dlatego, że wykonanie tego rozkazu następuje na końcu wykonania sekwencji rozkazów z pętli, przed ponownym wejściem na początek bloku rozkazów, wobec czego w obu przypadkach wartość iteratora zostanie zmieniona nie wpływając na ew. odwołania do niego w trakcie działania pętli. Szczególnym przypadkiem byłoby wywoływanie funkcji zamiast prostej modyfikacji wartości, ale jest to już przypadek szczególny..
Wartości iteratora pętli można swobodnie zmieniać w trakcie bloku rozkazów w pętli, z tym, że trzeba rozważyć skutek takiego działania czy jest on zgodny z naszymi potrzebami, bowiem poniższy przykład:
for (int a=0; a<10; a++) printf("Wartość a=%d\n\r",a--);
będzie skutkować tym, że pętla nigdy się nie skończy, jako że w trakcie wykonywania bloku rozkazów zmniejszamy iterator o 1, a na kroku iteracji zwiększamy o 1 – stąd pozostaje on niezmienny z wartością z punktu wejścia i nigdy nie osiągnie warunku wyjścia z pętli.
Na tym etapie rozważań można wprowadzić dwa dodatkowe słowa kluczowe służące do sterowania ewaluacji pętli (loop statement) i są to break oraz continue.
Polecenie break powoduje przerwanie wykonywania obecnego bloku pętli (czyli bloku rozkazów ograniczonego klamrami { } ) i “przeskok” do pierwszego następującego po tym bloku rozkazu. Taką konstrukcję można było zaobserwować już w przypadku bloku wyboru warunkowego switch/case.
Polecenie continue powoduje przerwanie wykonywania sekwencji rozkazów w danym bloku i powrót na początek bloku do pierwszego rozkazu. Może to być przydatne jako specyfikator rozdziału wykonywania sekwencji rozkazów jak na przykład w poniższym programie:
for (int a=0; a<10; a++) { if (a%2==0) continue; printf("Wartość a=%d\n\r",a); }
Jest to sposób aby wypisać jedynie nieparzyste wartości zmiennej a. Jest także sprawą oczywistą, że powyższy program da się zapisać bez używania continue jako:
for (int a=0; a<10; a++) { if (a%2!=0) printf("Wartość a=%d\n\r",a); }
Tym niemniej, jeśli z jakiś względów użycie continue czy break byłoby wskazane powodując np. lepszą czytelność kodu, można je oczywiście stosować.
Zadania do samodzielnego wykonania: Poniżej są załączone inne przykłady pętli for. Należy zastanowić się nad ich funkcjonalnością, rozważyć konstrukcję i działania takiej pętli, a następnie przetestować ich działanie samodzielnie:
//-------------------------------------------------------// int a=0; for ( ; ; ) if (a<10) printf("Wartość a=%d\n\r",a++); else break; //-------------------------------------------------------// int a; for (a=9; ;a--) if (a>=0) printf("Wartość a=%d\n\r",a); else break; //-------------------------------------------------------// for (char a='a';a!='z'+1;a++) //<- typ char jest także arytmetyczny! printf(" %c",a); //-------------------------------------------------------// for (float f=0.f;f<10;f+=0.1) printf("Wartość %f\n\r",f); //-------------------------------------------------------// for (float f=0.f;f<10;f+=0.01) //<- co oznacza zapis 0.f?? printf("Wartość %f\n\r",f); //-------------------------------------------------------// for (float f=0;f!=10;f+=0.1) //<-ciekawy przykład! Co się stało?! printf("Wartość %f\n\r",f); //-------------------------------------------------------//
Specyficzną modyfikacją (i bardzo przydatną) pętli for jest pętla for each – jednakże ta konstrukcja zostanie omówiona przy okazji zagadnień związanych z tablicami w następnym temacie.
while () {};
Kolejnym rodzajem pętli jest pętla while – jej działanie polega na tym, że wpierw badany jest warunek wykonania pętli i jeśli wynikiem operacji logicznej jest true to następuje wykonanie komendy, bądź bloku { } komend. Najważniejszą cechą tej pętli jest fakt, że jeśli warunek wejścia do pętli jest niespełniony to blok rozkazów { } nie wykona się ani razu.
Należy zauważyć, że ta pętla nie ma dedykowanego iteratora, co więcej nie musi go mieć. Wartość warunku decyduje o tym czy następują kolejne iteracje, czy też następuje opuszczenie bloku pętli. Ten rodzaj pętli wykorzystuje się przede wszystkim do obróbki danych zanim ich użyjemy w dalszym przetwarzaniu kodu. Obróbki, która ma zagwarantować, że pozyskane czy obliczone dane są dokładnie takie jakie potrzebujemy czy wymagamy dla poprawnego działania programu.
Konstrukcja takiej pętli jest zgodna z poniżej przedstawionymi przykładami:
#include <windows.h> #include <iostream> #include <ctime> int main() { srand(time(0)); //uruchomienie generatora liczb pseudolosowych while (1) { printf("Wylosowano : %5d",rand()); // losowanie liczby Sleep(1000); // zatrzymaj program na 1000ms <windows.h> system("cls"); // wykonaj polecenie systemowe cls } return 0; }
Powyższy program losuje wartości za pomocą generatora liczb pseudolosowych wyświetlając w nieskończoność wylosowane liczby z zakresu od 0 do RAND_MAX (literał zdefiniowany w bibliotece cstdlib). Przerwać taki program można albo klawiszami Ctrl-C, bądź zamykając okienko.
#include <windows.h> #include <iostream> #include <ctime> int main() { int a=100; while (a) { printf("Countdown : %10d",a--); Sleep(1000); system("cls"); } return 0; }
Poniższy jednak kod programu jest przykładem niewłaściwego wykorzystania pętli while () {}. Analiza kodu pokazuje, że wartość a jest dopiero losowana w pętli i nie może wcześniej posiadać wartości wymaganej w warunku. Do takiego rodzaju zadania dedykowana jest ostatnia ze struktur pętli do {} while () opisana poniżej.
#include <iostream> #include <ctime> int main() { srand(time(0)); int a,cel=1000; long long i=0; while (a!=cel) { printf("Wylosowano liczbę : %5d \n\r",a=rand()); system("cls"); i++; } printf("wylosowano liczbę %d po %lld powtórzeniach pętli",cel,i); return 0; }
do {} while ();
Ostatnią z omawianych pętli jest do while – jest ona pętlą również pozbawioną jawnego iteratora. Różni ją od poprzedniej miejsce w którym zostaje sprawdzony warunek, a mianowicie jest on sprawdzany na końcu po wykonaniu bloku rozkazów. Oznacza to, że w tym rodzaju pętli blok rozkazów zostanie wykonany przynajmniej raz. Stosuje się taką pętlę w przypadku, gdy chcemy zagwarantować, że dany kod wykona się przynajmniej raz – np. gdy obliczamy jakąś wartość i jeśli przy zadanych parametrach udało się to nam za pierwszym razem – program opuszcza pętlę, jeśli jednak należy dalej przetwarzać dane – to następuje kolejna iteracja bloku rozkazów.
Opisane poprzednio niewłaściwe użycie pętli while () {} obecnie przyjmie poprawnie skonstruowaną strukturę programu przedstawioną poniżej:
#include <iostream> #include <ctime> int main() { srand(time(0)); int a,cel=500; long long i=0; do { printf("Wylosowano liczbę : %5d \n\r",a=rand()%1000); // 0-999 system("cls"); i++; } while (a!=cel); printf("wylosowano liczbę %d po %lld powtórzeniach pętli",cel,i); return 0; }
Powyższy kod jest również ciekawym przykładem tego jak losowym jest wbudowany generator liczb pseudolosowych.
Wszystkie przedstawione pętle mogą być swobodnie zagnieżdżane tzn. jedna, zewnętrzna pętla okala strukturę pętli wewnątrz. Poziom zagnieżdżenia jest dowolny, uzależniony od naszych potrzeb i możliwości obliczeniowych maszyny (ilości pamięci).
Rekurencja
Zupełnie innym rodzajem pętli, ale możliwym do zrealizowania jest tzw. pętla rekurencyjna. Aby ją zrealizować musimy poznać konstrukcję metod (funkcje czy procedury). Ta pętla często jest przydatna przy bardziej zaawansowanych projektach, na tym etapie stanowi jednak jedynie ciekawostkę programistyczną opisaną osobno w Dodatku.
Zadanie 1: Napisz program, który wypisze wszystkie litery alfabetu łacińskiego (bez polskich liter).
Zadanie 2: Napisz program, który wyświetli tabliczkę mnożenia w sposób jak najbardziej zbliżony do tego.
Zadanie 3: Napisz program, który będzie losował (skorzystaj jeśli potrzebujesz z dodatku o liczbach losowych) wartość od 1 do 6, a następnie w sposób estetyczny wykreuje w konsoli widok typowej kostki do gry w stylu analogicznie jak dla wylosowanej wartości 5:
* *
*
* *