Pętle w języku C++ - pętla for

Pętle - sens istnienia

Pętle umożliwiają powtórzenie pewnych instrukcji dopóki nie zostanie spełniony warunek kończący pętlę. Dzięki temu możemy w bardzo łatwy i krótki sposób wypisać na przykład ten sam (lub podobny) komunikat kilka lub kilkaset razy lub pobrać od użytkownika na przykład 100 zmiennych. Nie będzie to stanowiło już większego problemu i dzięki temu, korzystanie z języka C++ stanie się wygodniejsze i przyniesie nowe możliwości.

Pętla for - podstawy

Pętla for podobnie jak wszystkie pozostałe pętle umożliwi nam powtórzenie określonych operacji tak długo jak warunek końcowy jest spełniony. Schematycznie pętlę for można zapisać:

for (stanyPoczatkowe; warunekKoncowy; zmiany)
  lista_instrukcji

Schematyczna forma pętli for, jak się zapewne domyślasz, sugeruje, że zarówno stanów początkowych jak i zmian może być kilka lub nawet kilkanaście. Poszczególne elementy oddzielimy wtedy przecinkami.

Oto prosty program przedstawiający wykorzystanie pętli for:

#include <iostream>

using namespace std;

int main()
{
 int licznik;
 for (licznik=1;licznik<10;++licznik)
    cout <<"Wykonuje petle po raz "<<licznik<<endl;
   
 cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
 getchar();
 return 0;
}    
program nr 13.1

W przykładzie tym mamy tylko jeden stan początkowy, jeden warunek końcowy i tylko jedną zmianę. Lista instrukcji (czyli w tym przypadku wypisanie tekstu na ekran) wykonuje się tak długo jak warunek (czyli stan końcowy) jest prawdziwy. Kiedy warunek staje się fałszywy, wówczas pętla zostaje zakończona. Przy każdym wykonaniu pętli zmienna licznik (jest to tak zwana zmienna sterująca pętlą) zostaje zmieniona tak jak to zostało zapisane w liście zmiana - czyli w naszym przypadku o 1.


Przede wszystkim zauważ, że w tym programie wykonujemy tylko jedną instrukcję w pętli. Gdybyśmy chcieli wykonać więcej instrukcji, musimy je otoczyć nawiasami klamrowymi.


Przy okazji zwróć uwagę, że w linii, w której została zapisana lista stanów początkowych, warunków końcowych i zmian, nie ma średnika. Jeśli postawisz tam średnik, kompilator nie poinformuje Cię o błędzie, ale program nie wykona się tak, jak tego oczekujesz.


Zwróć również uwagę na zapisaną zmianę: ++licznik. Wiesz oczywiście, że znaczy to to samo, co licznik+=1. W większości książek dotyczących programowania spotkasz jednak w tym miejscu zapis licznik++. Jak zapewne pamiętasz operator ++ można stosować zarówno przed jak i po zmiennej. Jak powinno być w tym przypadku? Otóż w tym przypadku nie ma to najmniejszego znaczenia z punktu widzenia logiki działania. Jednak operator ++ użyty przed zmienną działa szybciej niż użyty po zmiennej, dlatego też radzę Ci używać w tym przypadku zapisu przed zmienną.

Uproszczona postać pętli for

Musisz wiedzieć, że w języku C++ możemy zrezygnować z dowolnej części pętli for, a nawet z ich wszystkich. Możemy pominąć zarówno stan początkowy, warunek końcowy, jak i regułę zmian podczas działania pętli. Pomijając dowolną regułę, średniki muszą jednak pozostać, aby można było odróżnić, która z części została pominięta - zatem zawsze pozostaną dwa średniki.


Pomijając jednak niektóre z reguł sprawiasz, że może się zdarzyć, że program się "zapętli" - to znaczy, że nasza pętla będzie zawsze prawdziwa. W normalnym programie oczywiście nie możesz sobie na to pozwolić, jednak na razie warto o tym wiedzieć i zdawać sobie sprawę.


Poniższe programy przedstawiają jak można pomijać poszczególne części pętli for. Jeśli program nic nie robi, albo wypisuje to samo (czyli się zapętlił), przerwiesz jego działanie naciskając ctrl + Break lub ctrl + C w systemie MS Windows.


W poniższym programie pominęliśmy każdą z części - pętla wykonuje się w nieskończoność. Użytkownik musi nacisnąć kombinację klawiszy ctrl + Break lub ctrl + C aby przerwać działanie programu:

#include <iostream>

using namespace std;

int main()
{

 for (;;)
 {
    cout <<"Petla nieskonczona"<<endl;
    cout <<"Nacisnij ctrl + c lub ctrl + pause"<<endl;
 }

 return 0;  
}
program nr 13.2

Z kolei następny program przedstawia sytuację bardzo często spotykaną - pomijana jest lista stanów początkowych, bowiem takie stany ustaliliśmy już przed pętlą. Zauważ ponadto, że pętla wykonuje się 11 razy, a w środku pętli wypisujemy zmienną pętli zwiększoną o 1. Dlaczego? Z prostego powodu - pętlę zaczęliśmy od 0 - jednak trochę dziwnie byłoby wypisać, że pętla wykonuje się "zerowy raz", prawda? Zwróć jednak uwagę, że wypisanie i+1 nie zmienia wartości i (i+1 to nie to samo co i=i+1).

#include <iostream>

using namespace std;

int main()
{

 unsigned int i=0;
 for (;i<=10;++i)
 {
    cout <<"Wykonuje sie ";
    cout <<i+1<<" raz"<<endl;
 }

 cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
 getchar();
 return 0;
}
program nr 13.3

Oczywiście może zostać pominięta również część dotycząca zmian podczas wykonywania pętli, jednak ten przypadek omówię oddzielnie. Jak widzisz istnieje dość sporo sposobów wykorzystania części definiujących zachowanie pętli for - z jednych sposobów korzystamy częściej, a z innych rzadziej, jednak uwierz mi, że każdy z nich jest przydatny.


Przy okazji zwróć uwagę na to, że trzeba być bardzo ostrożnym podczas pisania pętli - bardzo mały błąd i już zapętlimy program - będzie się on wykonywał w nieskończoność, a zazwyczaj nie jest to naszym celem.

Zmienna sterująca pętlą

Musisz wiedzieć, że zmienną, która występuje w stanach początkowych lub w warunku końcowym nazywamy zmienną sterującą pętlą - bowiem od stanu tej zmiennej zależy, jak długo pętla będzie się wykonywać. Jeśli mamy kilka stanów początkowych, to tak naprawdę zmiennych sterujących pętlą może być kilka, jednak do tej pory w przykładach stosowaliśmy zawsze jedną zmienną sterującą.


Jedną z zalet języka C++ jest to, że zmienną sterującą pętli nie musimy koniecznie zmieniać w liście zmian, możemy tak samo to wykonać jako zwykłą instrukcję. Przykładowo, pierwszy program znajdujący się w tej lekcji możemy zapisać następująco:

#include <iostream>

using namespace std;

int main()
{
 int licznik;
 for (licznik=1;licznik<10;)
 {
    cout <<"Wykonuje petle po raz "<<licznik<<endl;
    ++licznik;
 }
 cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
 getchar();
 return 0;  
}
program nr 13.4

Dodatkowo, możemy połączyć dwa sposoby - zmienną sterującą pętlą możemy zwiększać zarówno na liście zmian, jak i wewnątrz samej pętli. W ten sposób możemy uzyskać bardzo ciekawe efekty. W zależności od pewnych zdarzeń, możemy pomijać pewne zakresy zmiennej sterującej. W nieco bardziej skomplikowanych programach, taka cecha jest bardzo przydatna. Oto 2 przykładowe pętle:

 int licznik;
 for (licznik=1;licznik<10;++licznik)  
 {
    cout <<"Wykonuje petle po raz "<<licznik<<endl;
    ++licznik;
 }

Za pomocą powyższej pętli zwiększamy tak naprawdę zmienną sterującą pętli o 2 w każdym kroku. Raz wykonujemy to na liście zmian, a raz jako zwykłą instrukcję.

 int licznik;
 for (licznik=1;licznik<10;++licznik)  
 {
    cout <<"Wartosc zmiennej licznik wynosi "<<licznik<<endl;
    if (licznik==2 || licznik==5)
       licznik+=2;
 }

Z kolei w powyższej pętli zmienna sterująca jest zawsze zwiększana o 1. Jeśli jednak ta zmienna wynosi 2 lub 5, jest wówczas zwiększana dodatkowo o 2.


Przy okazji warto zwrócić uwagę na to, gdzie deklarujemy zmienną sterującą pętlą. Zmienną sterującą pętlą możemy deklarować tak jak każdą inną zwykłą zmienną w programie, tzn. na początku funkcji main lub przed samą pętlą.


Możemy to jednak również zrobić bezpośrednio w pętli. Przy okazji, chcę zwrócić uwagę, że nie chodzi mi o to, gdzie zmiennej przypiszemy początkową wartość (zainicjalizujemy), tylko o to, gdzie napiszemy, że zmienna jest danego typu.

Oba podejścia mają swoje zalety i wady. Zacznijmy od przyjrzenia się obu metodom:

Metoda 1:

 int licznik;
 for (licznik=1;licznik<10;++licznik)  
 {
    cout <<"Wykonuje petle po raz "<<licznik<<endl;      
 }
 cout <<"Wartosc licznika po wykonaniu petli to: "<<licznik;

Metoda 2:

 for (int licznik=1;licznik<10;++licznik)  
 {
    cout <<"Wykonuje petle po raz "<<licznik<<endl;      
 }
 /*
    Tutaj zmienna licznik juz nie istnieje. Gdyby instrukcja wypisania
    nie byla w komentarzu, kompilator zasygnalizuje blad
 */

//   cout <<"Wartosc licznika po wykonaniu petli to: "<<licznik;

Druga metoda jest tak naprawdę znacznie częściej stosowana przez początkujących programistów. Zmienna zostaje utworzona tylko na czas działania pętli i wartość zmiennej znana jest tylko wewnątrz pętli. Natomiast już po zakończeniu pętli, wartości uzyskać nie można, bowiem po zakończeniu pętli, taka zmienna już nie istnieje.


Mimo tak oczywistych wad, metoda ma jedną zaletę - stosując tą metodę, tuż po zakończeniu pętli wartość zmiennej i sama zmienna nie są znane - dzięki temu zapobiegamy zaśmiecaniu programu zmiennymi, które są używane tylko w pętlach.


Pierwsza metoda natomiast zostanie wykorzystana przez bardziej świadomego i wymagającego programistę. Przede wszystkim, w wielu programach, aby sprytnie wykonać pewną czynność, często przydaje się znać wartość zmiennej tuż po zakończeniu pętli.


Na dodatek warto zdawać sobie sprawę, że jeśli w programie umieścimy 100 pętli, możemy za każdym razem skorzystać z tej samej zmiennej. Dzięki temu zmienna jest tworzona tylko raz, natomiast w metodzie drugiej zmienna będzie tworzona za każdym razem. Mimo, że dla Ciebie nie jest to widoczne, z pewnością zadziała to szybciej. Z drugiej jednak strony, w programie możemy uzyskać niepotrzebne zmienne, które będą nam tylko "przeszkadzały".


To której metody będziesz używać, zależy tylko i wyłącznie od Ciebie. Ja w przypadkach większych programów, staram się stosować metodę pierwszą, bowiem zadziała ona szybciej. Niekiedy jednak, zwłaszcza, gdy w programie nie mamy aż tak wielu pętli i potrzeby znania wartości zmiennej po zakończeniu pętli, można skorzystać z metody drugiej.

Dwie zmienne sterujące pętlą

Jak już wspomniałem, pętlą mogą sterować dwie (lub więcej) zmiennych. Wówczas w części stanów początkowych oraz zmian, zapisujemy instrukcje dotyczące poszczególnych zmiennych rozdzielając je przecinkiem.


Warto jednak zwrócić uwagę, że warunek końcowy pozostaje tylko jeden. Tylko w taki sposób pętla zadziała. Jeśli pomyślisz, że w warunku końcowym można rozdzielać warunki dotyczące poszczególnych zmiennych za pomocą przecinka, zobaczysz, że program nie działa zgodnie z oczekiwaniami (chyba, że wybierzesz szczególny przypadek).


Dlatego zapamiętaj sobie dobrze - w pętli for mamy tylko jeden warunek końcowy. Warunek końcowy może być jednak rozbudowany za pomocą operatorów logicznych, na przykład tak jak w poniższym programie:

#include <iostream>

using namespace std;

int main()
{
 int i,j;
 for (i=1,j=5;i<6 && j<=10;++i,j+=2)
    cout <<"i wynosi "<<i<<" a j wynosi "<<j<<endl;

 cout <<"Po zakonczeniu petli i wynosi "<<i<<" a j wynosi "<<j<<endl;

 cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
 getchar();
 return 0;  
}
program nr 13.5

W powyższym przykładzie, przypisujemy początkowe wartości zmiennym sterującym. W każdym kroku zmieniamy też nasze zmienne sterujące: zmienną i zwiększamy o 1, a zmienną j zwiększamy o 2. Warunek jest tylko jeden, ale jest to warunek złożony - uwzględnia wartości obu zmiennych sterujących.

Typ bez znaku - mała pułapka

Jeśli dokładnie śledzisz wszystkie przykłady, które pojawiły się do tej pory, to na pewno udało Ci się zauważyć pewną charakterystyczną cechę. Mianowicie, po zakończeniu pętli, zmienna sterująca ma wartość większą niż to zostało określone w warunku końcowym. Wynika to z tego, że aby pętla została przerwana, warunek końcowy musi być niespełniony. Zatem wykonywana jest dodatkowo jeszcze jedna sekcja zmiana. Wówczas okazuje się, że warunek już nie jest prawdziwy i pętla zostaje zakończona.


Co prawda na razie nie będziemy korzystać z wartości zmiennej po zakończeniu pętli, jednak mimo to, uda się w ten sposób wyjaśnić pewien problem (tajemniczy dla niektórych początkujących).


Załóżmy, że chcemy wypisać liczby od 0 do 10. Możemy to zrobić tak:

  int i;
  for (i=0;i<=10;++i)
     cout <<i<<' ';

Dla formalności dodam, że ktoś oczywiście mógł wpaść na pomysł, żeby zrobić to w następujący sposób:

  int i;
  for (i=1;i<=11;++i)
     cout <<i-1<<' ';

Nam jednak będzie chodziło o pierwszy sposób - wypisywana liczba ma być równa wartości zmiennej sterującej pętlą.


Teraz dla odmiany załóżmy, że chcemy wypisać te same liczby, ale tym razem od 10 do 0. Oczywiście nie powinno to być żadną trudnością:

  int i;
  for (i=10;i>=0;--i)
     cout <<i<<' ';

Jeśli przypomnisz sobie lekcję o modyfikatorach, to zauważysz, że w obu występujących tutaj przykładach, można by było zastosować modyfikator unsigned. W obu bowiem przypadkach interesują nas liczby powyżej 0 i równe liczbie 0, więc nie warto umożliwić przechowywania liczb ujemnych, bowiem możemy chcieć tą przestrzeń wykorzystać do przechowywania większych liczb całkowitych dodatnich.


Bardzo często właśnie przy pętlach wykorzystuje się modyfikator unsigned. Prawie każdy tak robi, więc dlaczego Ty nie? Otóż zastosowanie modyfikatora w jednym z poniższych przykładów sprawi problem.


Tutaj zastosowaliśmy modyfikator unsigned, gdy wypisujemy liczby w sposób rosnący:

  unsigned int i;
  for (i=0;i<=10;++i)
     cout <<i<<' ';

Ten przypadek nie sprawia żadnych problemów. Modyfikator unsigned możemy stosować bez obaw, kiedy zmienna sterująca jest zwiększana.


Poniżej natomiast stosujemy modyfikator unsigned, gdy chcemy wypisać liczby w sposób malejący:

  unsigned int i;
  for (i=10;i>=0;--i)
     cout <<i<<' ';

Jeśli uruchomisz powyższy kod, pamiętaj, że aby przerwać działanie pętli należy użyć ctrl + Break lub ctrl + C w systemie MS Windows.


Jeśli zastanawiasz się dlaczego tak jest, że ten program się zapętla, weź pod uwagę 2 rzeczy. Przede wszystkim modyfikator unsigned sprawia, że można w danej zmiennej przechowywać liczby nieujemne, czyli najmniejszą liczbą, którą można przechowywać jest 0.


Z drugiej strony, przypomnij sobie stwierdzenie, że zmienna po zakończeniu pętli ma wartość mniejszą niż to wynika z warunku. W przykładzie mamy warunek: i>=0 zatem za ostatnią poprawną wartość uważamy 0. Jednak ponieważ zmienna przyjmuje zawsze wartość mniejszą niż to wynika z warunku, będzie się starała przyjąć wartość mniejszą od zera, a konkretnie -1 (bo mamy --i w sekcji zmiany).


Tutaj pojawia się właśnie problem - do liczby z modyfikatorem unsigned usiłujemy przypisać liczbę -1, czyli liczbę ujemną. W efekcie liczbie zostaje przypisana największa z możliwych liczb całkowitych (czyli bardzo duża liczba) i znowu wartość zmiennej i będzie się zmniejszać aż do 0. W rezultacie, pętla nigdy nie zostanie zakończona.


W tym przypadku wychwycenie błędu było bardzo łatwe, jednak rzadko kiedy wypisujemy w pętli wartość zmiennej. Zazwyczaj wykonujemy inne operacje i wykrycie takiego błędu, zwłaszcza przez początkującego programistę, może nie być takie łatwe.

Pętle zagnieżdżone

Jak zapewne udało Ci się zauważyć, we wszystkich przykładach dotyczących pętli for, dokonywaliśmy na razie jedynie prostych operacji. Operacje te mogą być jednak znacznie bardziej skomplikowane. W szczególności, instrukcją wykonywaną w pętli for, może być kolejna pętla for.


Jednak jeśli stosujemy pętle zagnieżdżone, w każdej pętli zmienna sterująca powinna być inna. W przeciwnym przypadku uzyskamy wyniki, które mogą być nieoczekiwane. Użycie takiej samej nazwy zmiennej sterującej w obu pętlach jest dość częstym błędem popełnianym przez początkujących programistów.


Przykładowe zagnieżdżone pętle for znajdują się w poniższym programie - od razu zauważ, że w pierwszej pętli zmienna sterująca to i, natomiast w drugiej pętli zmienną sterującą jest zmienna j.


Postaraj się przeanalizować i zrozumieć poniższy program na własną rękę - w przyszłości będziesz rozwiązywać problemy samemu, więc na razie postaraj się chociaż zrozumieć jak dokładnie działa program.

#include <iostream>

using namespace std;

int main()
{
 int ktory=1, i, j;

 for (i=1;i<=10; i+=2)
 {
    cout <<"Tak bedzie dla i="<<i<<endl;
    for (j=5;j>=2;--j)
    {
       cout <<ktory<<". raz: i*j="<<i*j<<endl;
       ++ktory;
    }
    cout <<"Tak bylo dla i="<<i<<endl;
 }

 cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
 getchar();
 return 0;  
}
program nr 13.6

Jeśli nie udało Ci się zrozumieć jak działa powyższy program lub nie wiesz na pewno, że dobrze myślisz, możesz kontynuować czytanie tej strony.


Przede wszystkim zmienna ktory służy nam do określenia, ile razy dokonaliśmy wypisania iloczynu zmiennych i oraz j. Dlatego też odpowiednio ją zwiększamy w wewnętrznej pętli (zauważ wewnętrznej a nie zewnętrznej). Zmiennej tej mogłoby nie być, jednak używamy jej jako zmiennej niosącej dodatkowe informacje.


Pierwsza pętla przebiega wartości 1,3,5,7,9, druga natomiast 5,4,3,2. Zauważ też jeszcze raz to, o czym wspominałem już wcześniej: zmienne zastosowane w obu pętlach są inne: zmienne i oraz j.


Działanie odbywa się w następujący sposób: Zmiennej i przypisujemy 1 (inicjalizacja w pętli for) i sprawdzamy warunek pętli (i<=10). Ponieważ warunek jest prawdziwy, wykonujemy listę instrukcji. Poza zwykłymi wypisaniami na ekran, wykonujemy drugą pętlę for (tę ze zmienną j).


W tej pętli j przebiega wartości od 5 do 2. Za ostatnim razem, gdy zmienna j osiągnie wartość 1, pętla ta nie zostanie już powtórzona.


Teraz czas zmienić zmienną i (tę od zewnętrznej pętli), czyli i+=2. Zmienna i ma teraz wartość 3. Dalsze działanie powinno już być dla Ciebie oczywiste, bowiem dzieje się w analogiczny sposób.


Ostatni przykład mógł wydać Ci się dosyć trudny, jednak tego typu zagnieżdżenia pętli są codziennością w pisaniu jakichkolwiek programów. Takich pętli zagnieżdżonych może być dużo więcej - poprzednio były tylko dwie, jednak zazwyczaj w programach używa się właśnie 2 lub 3 zagnieżdżonych pętli.


Mimo że jak już wspomniałem, w zagnieżdżonych pętlach powinny być używane różne zmienne, nie jest zabronione używanie zmiennej z pętli zewnętrznej. Czasami nawet jej wartość może się przydać z takich czy innych powodów.


W poniższym przykładzie wypisujemy iloczyny liczb od 1 aż do liczby, którą się obecnie "zajmujemy" (symbolizuje ją zmienna i). Zauważ, że zastosowaliśmy modyfikator unsigned (bowiem zwiększamy zmienną w kierunku rosnącym i nie musimy obawiać się wartości -1) oraz, że obie zmienne sterujące zostały utworzone bezpośrednio w pętlach i nie są znane po zakończeniu pętli (bo nie zależy nam na tym, żeby były znane).

#include <iostream>

using namespace std;

int main()
{  
 for (unsigned int i=1;i<=10;i+=1)
 {
    for (unsigned int j=1;j<=i;j++)
       cout <<i*j<<' ';
    cout <<endl;
 }
 cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
 getchar();
 return 0;  
}
program nr 13.7

Jak widać w powyższym programie, w pętli wewnętrznej korzystamy ze zmiennej z pętli zewnętrznej (warunek j<=i). Tak więc to, ile razy powtórzymy pętlę wewnętrzną, zależy w ogromnej mierze od zmiennej i z pętli zewnętrznej. Z resztą wystarczy spojrzeć na wynik działania programu i przez chwilę przeanalizować kod źródłowy.


Muszę Ci również powiedzieć, że takie zastosowanie, mimo że nie jest bardzo często wykorzystywane, nie jest dla osoby programującej czymś trudnym do zrozumienia. Jeśli zatem nie rozumiesz tego przykładu, weź kartkę papieru i przeanalizuj jak się zmieniają poszczególne zmienne i co się dzieje w programie (ja właśnie bardzo często korzystam z tej metody).

Podsumowanie

Dzięki tej lekcji udało Ci się poznać pętlę for. Zagadnienia tutaj przedstawione są bardzo ważne i niezbędne w Twojej dalszej przygodzie z programowaniem. Należy sobie zdawać sprawę, że pętle są bardzo użyteczne. Z drugiej jednak strony, nieumiejętne ich użycie, może spowodować zapętlenie programu, dlatego trzeba być bardzo ostrożnym. W tej lekcji zwróciłem Twoją uwagę na rzeczy, których w większości książek nie przeczytasz. Mam zatem nadzieję, że wykorzystasz zdobytą tutaj dodatkową wiedzę w przyszłości.

Zadanie 1

Napisz program, który wypisuje liczby od 1 do 50, a następnie od 50 do 1.

Zadanie 2

Napisz program, który wyprowadzi znaki od A do Z.

Zadanie 3

Napisz program, który narysuje z gwiazdek (*) kwadrat 10 na 10

Zadanie 4

Napisz program, który obliczy sumę n kolejnych liczb naturalnych (począwszy od 1)

Zadanie 5

Napisz program, który obliczy silnię liczby N

Zadanie 6

Napisz program, który obliczy średnią N podawanych przez użytkownika liczb

powrót