Serwis Edukacyjny
w I-LO w Tarnowie
obrazek

Materiały dla uczniów liceum

  Wyjście       Spis treści       Wstecz       Dalej  

obrazek

Autor artykułu: mgr Jerzy Wałaszek
Konsultacje: Wojciech Grodowski, mgr inż. Janusz Wałaszek

©2024 mgr Jerzy Wałaszek
I LO w Tarnowie

obrazek

Warsztat

Kurs języka C

Pętle

SPIS TREŚCI
Podrozdziały

Co to jest pętla

Przez pętlę (ang. loop) rozumiemy cykliczne wykonywanie instrukcji. Wygląda to tak:

  instrukcja_1  
  instrukcja_2  
  ...  
  instrukcja_n  

Komputer wykonuje kolejno instrukcję_1, instrukcję_2, ..., instrukcję_n, po czym wraca i ponownie wykonuje instrukcję_1 i dalsze.

Praktycznie każdy program dla mikrokontrolera będzie zawierał pętlę. Bez niej mikrokontroler szybko wykonałby ciąg instrukcji w programie i co dalej? Zawiesiłby się, czyli przestał działać (w praktyce są stosowane rozwiązania, które usypiają mikrokontroler, gdy nie jest potrzebna jego aktywność dla oszczędzania baterii, a następnie wzbudzają w określonej sytuacji, ale czy nie jest to działanie w pętli?).

Zatem z pętlami będziesz musiał się zaprzyjaźnić. Nie ukrywam, że nie jest to problem łatwy dla początkujących i należy dużo poćwiczyć, aby był z tego pożytek. A zatem zabieramy się do pracy.


Na początek:  podrozdziału   strony 

Pętla warunkowa while

Przez pętlę warunkową (ang. conditional loop) rozumiemy pętlę, której wykonanie (tzn. wykonanie zawartych w niej instrukcji) uzależnione jest od spełnienia określonego warunku. Pierwszą pętlą warunkową, którą się zajmiemy, będzie pętla while (dopóki). Składnia tej pętli jest następująca:
while(warunek) instrukcja;

lub z instrukcją blokową:
while(warunek)
{
    instrukcja_1;
    instrukcja_2;
    ...
    instrukcja_n;
}

Działa to w sposób następujący:

Przed wykonaniem każdego obiegu pętli (czyli instrukcji lub bloku instrukcji) komputer zawsze wylicza wartość podanego za while warunku. Jeśli warunek jest prawdziwy (ma wartość różną od zera), to komputer wykonuje instrukcję pętli (lub blok instrukcji), po czym następuje powrót na początek pętli i wszystko się powtarza: znów jest obliczany warunek i jeśli prawdziwy, to następuje kolejny obieg pętli. Jeśli jednak warunek ma wartość 0, to instrukcja pętli (lub blok instrukcji) nie jest wykonywana i następuje wyjście z pętli, tzn. komputer wykonuje dalszą część programu za instrukcją while.

Pętle pozwalają zautomatyzować wiele operacji. Prześledźmy kilka przykładów:
/*
 Pętle warunkowe
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 27.09.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

int main()
{
  int a = 0;

  setlocale(LC_ALL,"");

  while(++a < 10) printf("%d\n",a);

  return 0;
}

W programie tworzymy jedną zmienną i nadajemy jej wartość 0. Następnie w pętli while testujemy warunek ++a < 10. Warunek ten jest wyliczany i znajduje się w nim operator modyfikacji ++. Zatem zmienna a jest zwiększana o 1 i wykonywany jest test, czy po modyfikacji zawartość zmiennej jest mniejsza od 10. Jeśli tak, to komputer wyświetla zawartość zmiennej a w kolejnych wierszach. Pętla będzie się wykonywać kolejno dla a = 0, 1, 2, ..., 9. Gdy po modyfikacji a osiągnie wartość 10, warunek while nie będzie spełniony i pętla nie będzie dalej wykonywana. W efekcie w oknie konsoli pojawi się ciąg liczb naturalnych od 1 do 9.

Zmień warunek w programie, tak aby były wypisywane liczby od 1 do 9999. Widzisz, jak prosta modyfikacja pętli pozwala rozszerzyć działanie programu!


Pętla typu while sprawdza swój warunek na początku każdego obiegu. Może się zatem zdarzyć, że nie wykona ani jednego obiegu, jeśli już na wejściu warunek będzie fałszywy.
/*
 Pętle warunkowe
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 28.09.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

int main()
{
  int a = 3;

  setlocale(LC_ALL,"");

  printf("START\n");
  while(--a > 3) printf("%d\n",a);
  printf("STOP\n");

  return 0;
}

W tym programie warunek --a > 3 jest fałszywy, ponieważ --a przyjmuje wartość 2 i nierówność nie będzie spełniona. Dlatego pętla while nie zostanie wykonana ani jeden raz.


Typowym problemem rozwiązywanym przez pętlę while jest znajdowanie największego wspólnego dzielnika algorytmem Euklidesa. Największym wspólnym dzielnikiem NWD (ang. Greatest Common Divisor GCD) dwóch liczb całkowitych a i b jest jest taka liczba c, która jednocześnie dzieli bez reszty liczbę a i liczbę b oraz nie istnieje liczba większa od c, które posiada tę samą właściwość. Euklides zauważył, że jeśli liczby a i b nie są równe, to liczba c (będąca ich największym wspólnym dzielnikiem) dzieli również ich różnicę:

Skąd to wiadomo? Spójrz na prawą część równości. Skoro c dzieli a i dzieli b, to ułamki a/c i b/c są liczbami całkowitymi. Skoro tak, to ich różnica również jest liczbą całkowitą. Zatem wynik dzielenia a-b przez c jest również liczbą całkowitą, stąd wnioskujemy, że liczba c musi dzielić różnicę a-b.

Co robił Euklides? Po prostu od większej liczby odejmował mniejszą, aż obie liczby się zrównały. Wtedy dowolna z nich była NWD. Zobaczmy to na przykładzie:
a b działanie
40 24 a ← 40 - 24 = 16
16 24 b ← 24 - 16 = 8
16 8 a ← 16 - 8 = 8
8 8  ← NWD

Uruchom program:

/*
 Pętle warunkowe
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 28.09.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

int main()
{
  int a = 40; // liczba naturalna
  int b = 24; // liczba naturalna

  setlocale(LC_ALL,"");

  printf("NWD(%d,%d) = ", a,b);

  while(a != b)
  if(a > b) a -= b;
  else b -= a;

  printf("%d\n",a);

  return 0;
}

Powyższy program działa wg algorytmu Euklidesa. Na początku pętli sprawdza, czy liczby a i b są różne. Jeśli tak, to od większej z nich odejmuje mniejszą i ponownie sprawdza warunek. Pętla while zostaje przerwana, gdy liczby się zrównają. Wtedy zostaje wypisana wartość NWD.

Gdy liczby a i b znacznie się różnią, np. a = 2000000000 i b = 1, to program musi wykonywać mnóstwo odejmowań. Można to łatwo zoptymalizować, jeśli przyjrzymy się dokładnie podanemu algorytmowi. Co on właściwie robi? Otóż odejmuje od liczby większej mniejszą tyle razy, ile może. To co zostanie jest mniejsze od odjemnika. Takie samo działanie uzyskamy za pomocą operacji reszty z dzielenia: od liczby większej mniejszą można odjąć tyle razy, ile się ona w niej mieści, czyli tyle, ile wynosi iloraz tych dwóch liczb. To, co zostanie w większej liczbie po odejmowaniu, jest resztą z dzielenia tych dwóch licz. Reszta ta idzie zawsze do liczby, która wcześniej była mniejsza. Do liczby większej przenosimy liczbę mniejszą (ponieważ teraz ona staje się liczbą większą). Operację przerywamy, gdy reszta z dzielenia przyjmie wartość 0. NWD mamy w liczbie większej (jeśli tego nie widzisz z opisu, rozpisz sobie to na kartce papieru, a wszystko stanie się jasne). Możemy zatem algorytm Euklidesa tak sformułować:

Dopóki b jest różne od zera, wykonuj: do a przenieś b, do b przenieś resztę z dzielenia a przez b. Wynik w a.

Zobaczmy na przykładzie:

a b działanie
40 24 a ← b; b ← reszta z a / b
24 16 a ← b; b ← reszta z a / b
16 8 a ← b; b ← reszta z a / b
8 0  ← NWD w a

Korzyść uzyskamy, gdy a i b będą się znacznie różnić. Na przykład:

a b działanie
2000000000 2 a ← b; b ← reszta z a / b
2 0  ← NWD w a

Uruchom program:

/*
 Pętle warunkowe
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 28.09.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

int main()
{
  int a = 40; // liczba naturalna
  int b = 24; // liczba naturalna
  int r; // reszta z dzielenia

  setlocale(LC_ALL,"");

  printf("NWD(%d,%d) = ", a,b);

  while(b)
  {
    r = a % b;
    a = b;
    b = r;
  }

  printf("%d\n",a);

  return 0;
}

Zwróć uwagę na warunek w pętli while. Jest tu sama zmienna b. Jest to najzupełniej poprawny warunek i czytaj go jako: dopóki b jest różne od zera. W pętli komputer wylicza najpierw resztę z dzielenia a przez b i zapamiętuje ją w zmiennej pomocniczej r. Następnie przesuwa wartości: do a idzie dzielnik, do b idzie reszta z dzielenia. Chociaż program wydaje się być dłuższym od poprzedniego, to jednak działa szybciej dla dużych różnic pomiędzy a i b i jest zalecaną wersją algorytmu Euklidesa obliczania NWD.


Jeśli warunek w pętli while jest zawsze prawdziwy, to otrzymujemy tzw. pętlę nieskończoną (ang. infinite loop lub endless loop). Pętla tego typu będzie się wykonywała w kółko.
/*
 Pętle warunkowe
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 28.09.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

int main()
{
  setlocale(LC_ALL,"");

  while(1) printf("Witamy w C ");

  return 0;
}

Program wypisuje w kółko tekst w pętli nieskończonej. Zatrzymujesz go kombinacją klawiszy Ctrl+C.

Z pętli nieskończonej można wyjść poleceniem break (patrz dalej).

Uwaga:

W praktyce nauczyciela często spotykam się z sytuacją, gdy uczniowie mylą ze sobą instrukcję warunkową if z pętlą warunkową while. Powodem być może jest podobna składnia:
if(warunek) instrukcja;

while(warunek) instrukcja;

Różnica jest jednak bardzo istotna:

if wykonuje warunkowo swoją instrukcję tylko jeden raz, nie ma tutaj powrotu do sprawdzania warunku.

while wykonuje warunkowo powtarzanie swojej instrukcji, po każdym obiegu pętli następuje powrót do sprawdzania warunku.


Na początek:  podrozdziału   strony 

Pętla warunkowa do...while

Drugą pętlą warunkową jest w języku C pętla do...while o następującej składni:
do instrukcja; while(warunek);

lub w wersji blokowej:
do
{
    instrukcja_1;
    instrukcja_2;
    ...
    instrukcja_n;
} while(warunek);

Instrukcja działa w sposób następujący:

Najpierw zostaje wykonana instrukcja (lub instrukcje w bloku). Następnie komputer wylicza wartość warunku. Jeśli jest różna od zera, to ponownie wykonuje instrukcję (lub blok instrukcji) i wraca do sprawdzenia warunku. Jeśli wartość warunku jest równa zero, to następuje wyjście z pętli i wykonanie następnej instrukcji w programie.

Uruchom program:
/*
 Pętle warunkowe
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 28.09.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

int main()
{
  int i = 1; // liczba naturalna
  int s = 0; // suma
  int g = 1000; // granica sumowania

  setlocale(LC_ALL,"");

  do printf("Suma = %4d\n",s += i++); while(s < g);

  return 0;
}

Program sumuje kolejne liczby naturalne aż do momentu, gdy suma osiągnie lub przekroczy zadaną granicę. Na początku tworzone są trzy zmienne:
i przechowuje kolejne liczby naturalne, 1,2,...
s suma kolejnych liczb naturalnych
g granica sumy

W zmiennej i  umieszczamy 1 (pierwsza liczba naturalna). Sumę zerujemy. W g  umieszczamy granicę, do której chcemy sumować. Następnie w pętli tworzymy kolejne sumy i wyświetlamy ich wartość. Zwróć uwagę na wyrażenie:
s += i++

Jest to wyrażenie, w którym modyfikacji ulegają dwie zmienne: s  oraz i. Język C (oraz jego potomek C++) jest chwalony przez programistów za swoją zwięzłość. Dzięki temu, że instrukcja modyfikacji posiada wartość, możemy jej użyć jako argumentu dla funkcji printf. W wyrażeniu do zmiennej s dodawana jest zawartość zmiennej i, po czym zmienna i  jest zwiększana o 1 (zwróć uwagę, że operator modyfikacji ++ jest umieszczony za nazwą zmiennej, zatem zwiększenie odbędzie się po pobraniu przez wyrażenie zawartości zmiennej i). Na końcu pętli sprawdzany jest warunek jej kontynuacji (czyli wykonania kolejnego obiegu). Jeśli wyliczona dotychczas suma jest mniejsza od granicy, to pętla się powtarza. Gdy s  osiągnie lub przekroczy wartość g, pętla jest kończona.


W wielu przypadkach pętle while i do...while można stosować z powodzeniem zamiennie. Jednak są sytuację, gdzie tego zrobić nie można. Pętlę do...while stosujemy najczęściej wtedy, gdy warunek sprawdzany na końcu pętli ma sens dopiero po wykonaniu zawartej w pętli instrukcji. Na przykład:

Podane wyżej zalecenia nie są ścisłe, ponieważ język C posiada niesamowitą elastyczność. Nie martw się, doświadczenie przyjdzie z czasem.

Musisz zwrócić uwagę na jedną ważną cechę różniącą obie pętle:


Na początek:  podrozdziału   strony 

Pętla iteracyjna for

Obieg iterowany jest to obieg, który posiada swój numer. Przez iterowanie rozumiemy wykonywanie w programie obiegów numerowanych. Pętla iteracyjna jest pętlą, która numeruje kolejno wykonane obiegi. Numer obiegu przechowywany jest w zmiennej zwanej licznikiem pętli (ang. loop counter). Pętlę iteracyjną można w prosty sposób zbudować przy pomocy pętli warunkowej typu while. Uruchom poniższy program:
/*
 Pętle warunkowe
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 28.09.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

int main()
{
  int i = 1; // licznik pętli

  setlocale(LC_ALL,"");

  while (i <= 20)
  {
    printf("Obieg nr %2d\n",i);
    i++;
  }

  return 0;
}

Zwróć uwagę na charakterystyczną budowę tej pętli:

int i = 1;   czynności przygotowawcze, najczęściej inicjalizacja licznika pętli, wykonywane przed pętlą
while(i <= 20)   sprawdzenie warunku kontynuacji, wykonywane na początku każdego obiegu
printf(...)   instrukcja wykonywana w każdym obiegu
i++   modyfikacja licznika pętli, wykonywana na końcu każdego obiegu

Ponieważ pętle iteracyjne są bardzo częstym elementem programów w języku C, istnieje instrukcja, która ułatwia ich stosowanie. Jest to instrukcja for o następującej składni:

for(a; b; c) instrukcja;

lub w postaci blokowej:

for(a; b; c)
{
    instrukcja_1;
    instrukcja_2;
    ...
    instrukcja_n;
}
a czynności przygotowawcze przed pętlą
b warunek kontynuacji sprawdzany przed każdym obiegiem
c czynności kończące obieg

Instrukcja for działa następująco:

Najpierw komputer wykonuje operację określoną przez parametr a. Najczęściej jest to przypisanie wartości początkowej zmiennej, która będzie pełniła rolę licznika pętli. Operacja ta jest wykonywana jednokrotnie przed rozpoczęciem pętli. Następnie komputer zaczyna wykonywać obiegi iterowane. Przed każdym obiegiem sprawdzany jest warunek b. Jeśli ma on wartość różną od zera, to obieg zostanie wykonany. Inaczej pętla zakończy działanie. W każdym obiegu jest wykonywana podana instrukcja (lub instrukcje w bloku). Na końcu każdego obiegu wykonywane jest działanie określone parametrem c. Najczęściej jest to operacja modyfikacji licznika pętli, aby w następnym obiegu miał inną wartość.

Instrukcja for grupuje w jednym miejscu wszystkie elementy sterujące wykonywaniem pętli iteracyjnej. Poniżej mamy poprzedni program w wersji z pętlą for:

/*
 Pętle iteracyjne
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 28.09.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

int main()
{
  int i; // licznik pętli
 
  setlocale(LC_ALL,"");

  for(i = 1; i <= 20; i++) printf("Obieg nr %2d\n",i);

  return 0;
}

Jak widzisz, program jest krótszy. Gdy nabierzesz wprawy, to okaże się, że stosowanie for zamiast while zwiększa czytelność programu.

Pobawmy się trochę pętlami iteracyjnymi. Numery obiegów nie muszą być kolejnymi liczbami naturalnymi: licznik pętli możesz modyfikować w dowolny sposób:

/*
 Pętle iteracyjne
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 28.09.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

int main()
{
  int i; // licznik pętli
 
  setlocale(LC_ALL,"");

  for(i = 0; i <= 20; i += 2) printf("Obieg nr %2d\n",i);

  return 0;
}

Licznik pętli możesz wykorzystywać do kontrolowania liczby obiegów, które wykona pętla:

/*
 Pętle iteracyjne
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 28.09.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

int main()
{
  int i; // licznik pętli
  unsigned p = 1; // potęgi 2

  setlocale(LC_ALL,"");

  for(i = 0; i <= 31; i++)
  {
    printf("2^%d = %u\n",i,p);
    p += p;
  }

  return 0;
}

Kolejny program oblicza tzw. liczby Fibonacciego, które powstają w następujący sposób:

f0 = 0  – pierwsza liczba ciągu Fibonacciego
f1 = 1 – druga liczba ciągu Fibonacciego
fi = fi-2 + fi-1 – każda kolejna liczba Fibonacciego powstaje jako suma dwóch poprzednich.

Oto kilka początkowych liczb Fibonacciego:

f0 = 0
f1 = 1
f2 = 0 + 1 = 1
f3 = 1 + 1 = 2
f4 = 1 + 2 = 3
f5 = 2 + 3 = 5
f6 = 3 + 5 = 8
f7 = 5 + 8 = 13
f8 = 8 + 13 = 21
f9 = 13 + 21 = 34
f10 = 21 + 34 = 55
...

Liczby Fibonacciego dosyć szybko rosną. Stosuje się je w zaawansowanej informatyce do rozwiązywania różnych problemów związanych ze strukturami danych i symulacjami.

Uruchom program:

/*
 Pętle iteracyjne
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 28.09.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

int main()
{
  int i; // licznik pętli
  int n = 40; // numer ostatniej liczby Fibonacciego
  unsigned f,f1,f2; // kolejno obliczane liczby

  setlocale(LC_ALL,"");

  for(i = 0; i <= n; i++)
  {
    if(i < 2) f = i; else f = f2 + f1;
    printf("f(%d) = %d\n",i,f);
    f2 = f1;
    f1 = f;
  }

  return 0;
}

W programie tworzymy zmienne:

i zlicza wykonane obiegi pętli iteracyjnej
n definiuje numer ostatniego obiegu
f i-ta liczba Fibonacciego
f1 (i-1) liczba Fibonacciego, czyli liczba poprzednia
f2 (i-2) liczba Fibonacciego, czyli liczba poprzednia poprzedniej

Pętla iteracyjna for wykona n + 1 obiegów o numerach od 0 do n. W każdym obiegu w zmiennej f obliczamy kolejną liczbę Fibonacciego o numerze równym numerowi obiegu pętli. Dla obiegów o numerach mniejszych od 2, czyli 0 i 1, liczba Fibonacciego ma wartość równą swojemu numerowi, czyli zawartości zmiennej i. To właśnie sprawdza instrukcja warunkowa if: w obiegu 0 i 1 do f trafi kolejno 0 i 1 z licznika pętli. Dla obiegów o numerach większych od 1 liczbę Fibonacciego liczymy jako sumę dwóch liczb poprzednich liczb Fibonacciego, które są pamiętane w zmiennych f1 i f2. Po wyliczeniu f program wyświetla wartość liczby Fibonacciego. Teraz występuje ciekawe miejsce: Dwie następne instrukcje przypisania mają na celu zapamiętanie dwóch liczb Fibonacciego: poprzedniej w zmiennej f2 i bieżącej w f1 w tej dokładnie kolejności. W następnym obiegu (o numerze większym od 1) liczby te zostaną wykorzystane do obliczenia następnej liczby Fibonacciego. Takie wykorzystywanie poprzednich obliczeń nosi nazwę programowania dynamicznego i znakomicie przyspiesza czas otrzymywania wyniku.


Pętlę for można wykorzystać również jako pętlę warunkową, którą w sumie jest (instrukcja for jest wewnętrznie rozwijana w odpowiednią pętlę while).

Uruchom program:
/*
 Pętle warunkowe
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 28.09.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

int main()
{
  int i; // liczby nieparzyste
  int g = 1000; // granica sumowania
  unsigned s = 0; // suma

  setlocale(LC_ALL,"");

  for(i = 1; s < g; i += 2)
  {
    s += i;
    printf("suma = %d\n",s);
  }

  return 0;
}

Program sumuje kolejne liczby nieparzyste, aż ich suma przekroczy zadaną granicę. Zwróć uwagę, że warunek kontynuacji nie jest związany z wartością licznika pętli. Otrzymujemy zatem normalną pętlę warunkową.


Niepotrzebne elementy w obszarze sterowania pętli for można pominąć, jednak należy zachować średniki. Pominięcie warunku kontynuacji spowoduje, że pętla będzie się wykonywała w nieskończoność (tzn. aż zatrzymamy program kombinacją klawiszy Ctrl+C).

/*
 Pętle warunkowe
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 28.09.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

int main()
{
  unsigned i = 0;

  setlocale(LC_ALL,"");

  for(; ; i++) printf("%10d",i);

  return 0;
}

Na początek:  podrozdziału   strony 

Pętle zagnieżdżone

Jeśli instrukcją powtarzaną w pętli będzie instrukcja pętli, to otrzymamy pętle zagnieżdżone. Przy zagnieżdżaniu pętli iteracyjnych należy stosować osobne zmienne liczników pętli.

Zobaczmy, jak to działa w praktyce.

Uruchom poniższy program:

/*
 Pętle zagnieżdżone
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 29.09.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

int main()
{
  int x = 15; // liczba znaków w wierszu
  int i; // licznik znaków w wierszu

  setlocale(LC_ALL,"");

  for(i = 1; i <= x; i++) printf("X");
  printf("\n");

  return 0;
}

Program wypisuje wiersz zbudowany z 15 znaków X:

XXXXXXXXXXXXXXX

Zmodyfikujmy nieco ten program dodając pętlę zewnętrzną, która będzie wykonywała 15 razy te dwie instrukcje:

/*
 Pętle zagnieżdżone
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 29.09.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

int main()
{
  int x = 15; // liczba znaków w wierszu
  int i; // licznik znaków w wierszu
  int j; // licznik wierszy

  setlocale(LC_ALL,"");

  for(j = 1; j <= x; j++)
  {
    for(i = 1; i <= x; i++) printf("X");
    printf("\n");
  }

  return 0;
}

W efekcie otrzymasz kwadrat (raczej prostokąt, ponieważ literki w oknie konsoli są wyższe niż szersze):

XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX

Pętla wewnętrzna wykonuje zawsze x obiegów. A gdybyśmy kazali się jej wykonywać j razy?

/*
 Pętle zagnieżdżone
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 29.09.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

int main()
{
  int x = 15; // liczba znaków w wierszu
  int i; // licznik znaków w wierszu
  int j; // licznik wierszy

  setlocale(LC_ALL,"");

  for(j = 1; j <= x; j++)
  {
    for(i = 1; i <= j; i++) printf("X");
    printf("\n");
  }

  return 0;
}

Drobna zmiana, a wynik inny:

X
XX
XXX
XXXX
XXXXX
XXXXXX
XXXXXXX
XXXXXXXX
XXXXXXXXX
XXXXXXXXXX
XXXXXXXXXXX
XXXXXXXXXXXX
XXXXXXXXXXXXX
XXXXXXXXXXXXXX
XXXXXXXXXXXXXXX

Na pewnym konkursie informatycznym zadanie polegało na napisaniu programu wypisującego w oknie konsoli taką oto figurę:

XXXXXXXXXXXXXXX
XX...........XX
X.X.........X.X
X..X.......X..X
X...X.....X...X
X....X...X....X
X.....X.X.....X
X......X......X
X.....X.X.....X
X....X...X....X
X...X.....X...X
X..X.......X..X
X.X.........X.X
XX...........XX
XXXXXXXXXXXXXXX

Jeden młody, ambitny człowiek bardzo się nad tym trudził i wyprodukował program zawierający mnóstwo różnych pętli. Tymczasem sprawa jest banalnie prosta pod warunkiem, że dokonamy pewnego spostrzeżenia. Otóż każdy znak, który pojawia się na wydruku możemy potraktować, jako znak znajdujący się w j-tym wierszu i w i-tej kolumnie:

kolumny → i
w
i
e
r
s
z
e

j
  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
1  X   X   X   X   X   X   X   X   X   X   X   X   X   X   X 
2  X   X   . . . . . . . . . . .  X   X 
3  X  .  X  . . . . . . . . .  X  .  X 
4  X  . .  X  . . . . . . .  X  . .  X 
5  X  . . .  X  . . . . .  X  . . .  X 
6  X  . . . .  X  . . .  X  . . . .  X 
7  X  . . . . .  X  .  X  . . . . .  X 
8  X  . . . . . .  X  . . . . . .  X 
9  X  . . . . .  X  .  X  . . . . .  X 
10  X  . . . .  X  . . .  X  . . . .  X 
11  X  . . .  X  . . . . .  X  . . .  X 
12  X  . .  X  . . . . . . .  X  . .  X 
13  X  .  X  . . . . . . . . .  X  .  X 
14  X   X  . . . . . . . . . . .  X   X 
15  X   X   X   X   X   X   X   X   X   X   X   X   X   X   X 

Figura zbudowana jest ze znaków "X" i ".". Najpierw skonstruujmy dwie pętle, które będą generować numery wierszy (pętla zewnetrzna) i kolumn (pętla zagnieżdżona). Wykorzystaj do tego celu poprzedni program:

for(j = 1; j <= 15; j++)
{
  for(i = 1; i <= 15; i++) printf("X");
  printf("\n");
}

W takiej postaci mamy szkielet programu, lecz program ten jeszcze nie produkuje naszej figury, ponieważ bezwarunkowo drukujemy na każdej pozycji (i,j) znak X.

XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX

Przyjrzyjmy się naszej figurze: w wierszach nr 1 i nr 15 są same X. Zmodyfikujmy ten program, tak aby w tych wierszach drukował" X"-y, a w pozostałych ".":

for(j = 1; j <= 15; j++)
{
  for(i = 1; i <= 15; i++)
    if((j == 1) || (j == 15)) printf("X");
    else                      printf(".");
  printf("\n");
}

Tak, stosujemy instrukcję warunkową, w której sprawdzamy numer aktualnego wiersza za pomocą wyrażenia logicznego. Jeśli jest to wiersz 1 lub 15, to drukujemy X. Jeśli nie, to drukujemy kropki. Teraz otrzymasz:

XXXXXXXXXXXXXXX
...............
...............
...............
...............
...............
...............
...............
...............
...............
...............
...............
...............
...............
XXXXXXXXXXXXXXX

Teraz dodajmy boki z X'ów. X'y mają się pojawiać, jeśli numer kolumny jest równy 1 lub 15:

for(j = 1; j <= 15; j++)
{
  for(i = 1; i <= 15; i++)
    if((j == 1) || (j == 15) ||
       (i == 1) || (i == 15)) printf("X");
    else                      printf(".");
  printf("\n");
}

Otrzymasz:

XXXXXXXXXXXXXXX
X.............X
X.............X
X.............X
X.............X
X.............X
X.............X
X.............X
X.............X
X.............X
X.............X
X.............X
X.............X
X.............X
XXXXXXXXXXXXXXX

Dodajmy przekątną. X'y mają się pojawiać dla j = i, czyli kolejny warunek:

for(j = 1; j <= 15; j++)
{
  for(i = 1; i <= 15; i++)
    if((j == 1) || (j == 15) ||
       (i == 1) || (i == 15) ||
       (j == i) ) printf("X");
    else          printf(".");
  printf("\n");
}

Otrzymasz:

XXXXXXXXXXXXXXX
XX............X
X.X...........X
X..X..........X
X...X.........X
X....X........X
X.....X.......X
X......X......X
X.......X.....X
X........X....X
X.........X...X
X..........X..X
X...........X.X
X............XX
XXXXXXXXXXXXXXX

I druga przekątna, tutaj musi być spełniony warunek j = 16 - i:

for(j = 1; j <= 15; j++)
{
  for(i = 1; i <= 15; i++)
    if((j == 1) || (j == 15) ||
       (i == 1) || (i == 15) ||
       (j == i) || (j == 16 - i)) printf("X");
    else                          printf(".");
  printf("\n");
}

I wynik ostateczny:

XXXXXXXXXXXXXXX
XX...........XX
X.X.........X.X
X..X.......X..X
X...X.....X...X
X....X...X....X
X.....X.X.....X
X......X......X
X.....X.X.....X
X....X...X....X
X...X.....X...X
X..X.......X..X
X.X.........X.X
XX...........XX
XXXXXXXXXXXXXXX

Jak widzisz, cały program zmieścił się w dwóch pętlach iteracyjnych. Jako ćwiczenie zmodyfikuj ten program, tak aby rysował figurę o rozmiarze n wierszy na n kolumn (n ma być zmienną).

Jeśli chcesz dalej poćwiczyć, to napisz programy rysujące w ten sposób figury 16 x 16. Będziesz musiał w nich zastosować dosyć złożone warunki.:

X.X.X.X.X.X.X.X.
.X.X.X.X.X.X.X.X
X.X.X.X.X.X.X.X.
.X.X.X.X.X.X.X.X
X.X.X.X.X.X.X.X.
.X.X.X.X.X.X.X.X
X.X.X.X.X.X.X.X.
.X.X.X.X.X.X.X.X
X.X.X.X.X.X.X.X.
.X.X.X.X.X.X.X.X
X.X.X.X.X.X.X.X.
.X.X.X.X.X.X.X.X
X.X.X.X.X.X.X.X.
.X.X.X.X.X.X.X.X
X.X.X.X.X.X.X.X.
.X.X.X.X.X.X.X.X
XXXX....XXXX....
XXXX....XXXX....
XXXX....XXXX....
XXXX....XXXX....
....XXXX....XXXX
....XXXX....XXXX
....XXXX....XXXX
....XXXX....XXXX
XXXX....XXXX....
XXXX....XXXX....
XXXX....XXXX....
XXXX....XXXX....
....XXXX....XXXX
....XXXX....XXXX
....XXXX....XXXX
....XXXX....XXXX
XXXX....XXXX....
.XXXX....XXXX...
..XXXX....XXXX..
...XXXX....XXXX.
....XXXX....XXXX
X....XXXX....XXX
XX....XXXX....XX
XXX....XXXX....X
XXXX....XXXX....
.XXXX....XXXX...
..XXXX....XXXX..
...XXXX....XXXX.
....XXXX....XXXX
X....XXXX....XXX
XX....XXXX....XX
XXX....XXXX....X
XXXXXXXXXXXXXXXX
.XXXXXXXXXXXXXX.
..XXXXXXXXXXXX..
...XXXXXXXXXX...
....XXXXXXXX....
.....XXXXXX.....
......XXXX......
.......XX.......
.......XX.......
......XXXX......
.....XXXXXX.....
....XXXXXXXX....
...XXXXXXXXXX...
..XXXXXXXXXXXX..
.XXXXXXXXXXXXXX.
XXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXX
X..............X
X.XXXXXXXXXXXX.X
X.X..........X.X
X.X.XXXXXXXX.X.X
X.X.X......X.X.X
X.X.X.XXXX.X.X.X
X.X.X.X..X.X.X.X
X.X.X.X..X.X.X.X
X.X.X.XXXX.X.X.X
X.X.X......X.X.X
X.X.XXXXXXXX.X.X
X.X..........X.X
X.XXXXXXXXXXXX.X
X..............X
XXXXXXXXXXXXXXXX

Poważniejszym przykładem zastosowania pętli zagnieżdżonych może być wyliczanie liczb pierwszych.

Liczba pierwsza (ang. prime number) jest liczbą naturalną p większą od 1, która dzieli się tylko przez 1 i przez siebie samą (czyli posiada dokładnie dwa różne podzielniki).

Odwracając tę definicję, otrzymamy, że liczba pierwsza jest liczbą naturalną p większą od 1, która nie dzieli się przez żadną z liczb naturalnych z przedziału od 2 do pierwiastka z p (z wyjątkiem liczby 2, która jest jedyną liczbą pierwszą parzystą). Ta druga definicja podaje sposób sprawdzenia, czy dana liczba jest pierwsza, czy złożona: Bierzemy kolejne liczby naturalne: jeśli p jest równe 2, to jest pierwsze, jeśli p jest większe od 2, to próbujemy je dzielić przez kolejne liczby z przedziału od 2 do pierwiastka całkowitego z p. Jeśli żadna z nich nie dzieli p, to mamy liczbę pierwszą. Jeśli którakolwiek dzieli p, to p nie jest liczbą pierwszą. Podzielność badamy sprawdzając resztę z dzielenia. Jeśli jedna liczba dzieli drugą, to reszta wynosi 0.

Uruchom program:

/*
 Pętle zagnieżdżone
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 29.09.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <locale.h>

int main()
{
  unsigned p; // liczba pierwsza
  unsigned a = 2; // dolna granica poszukiwań liczb pierwszych
  unsigned b = 3000; // górna granica poszukiwań liczb pierwszych
  unsigned pp; // przechowuje pierwiastek z p
  unsigned d; // dzielnik
  char test; // do testowania

  setlocale(LC_ALL,"");

  printf("Wyznaczanie liczb pierwszych w przedziale od %u do %u\n",a,b);

  for(p = a; p <= b; p++)
  {
    if(p == 2)
    {
      printf("%8u",p); // tego nie sprawdzamy
      continue;
    }
    pp = sqrt(p); // do pp pierwiastek całkowity z p
    test = 1;
    for(d = 2; d <= pp; d++)
      if(!(p % d))
      {
        test = 0; // p nie jest pierwsze
        break;
      }
    if(test) printf("%8u",p);
  }
  printf("\n");

  return 0;
}

W programie występują dwie pętle iteracyjne: pierwsza z licznikiem p i druga zagnieżdżona z licznikiem d. Pierwsza pętla w p generuje kolejne liczby naturalne od a do b. Teraz wewnątrz tej pętli sprawdzamy, czy p jest liczbą pierwszą. Najpierw wykonujemy test na 2. Jeśli p jest równe 2, to wyświetlamy p i tutaj pojawia się nowa instrukcja języka C, która jest związana z pętlami: continue. Powoduje ona przejście do następnego obiegu pętli z pominięciem dalszych instrukcji w danym obiegu. Tutaj jest to oczywiste. Jeśli p jest równe 2, to jest liczbą pierwszą i dalsze testy są zbędne – wyświetlamy p i kończymy ten obieg pętli. W następnym obiegu komputer wyznaczy nowe p, tym razem większe od 2, bo równe 3. Dla takich p obliczamy pierwiastek i próbujemy je dzielić w pętli wewnętrznej przez kolejne dzielniki d od 2 do pierwiastka z p. Tutaj jest drugi ciekawy fragment: jak sprawdzić, czy żaden dzielnik d nie dzieli naszej liczby p? Otóż wykorzystujemy zmienną pomocniczą test. Przed rozpoczęciem dzielenia, umieszczamy w niej dowolną wartość różną od 0, np. 1 jest dobre. Następnie uruchamiamy pętlę dzielącą. W każdym obiegu tej pętli sprawdzamy, czy aktualny dzielnik d dzieli p. Robimy to za pomocą instrukcji warunkowej if, która sprawdza, czy reszta z dzielenia p przez d jest równa 0. Jeśli tak, to d dzieli p i liczba p nie jest liczbą pierwszą. Zaznaczamy ten fakt zerując zmienną test i kończymy wykonywanie pętli wewnętrznej za pomocą instrukcji break. Jest to kolejna instrukcja języka C, która jest związana z pętlami. Jej wykonanie powoduje natychmiastowe wyjście z dowolnej pętli (wszelkie instrukcje w pętli umieszczone za break nie zostaną już wykonane). Tutaj też ma to sens, bo jeśli jakiś dzielnik d dzieli p, to p nie jest liczbą pierwszą i dalsze testowanie podzielności nie ma sensu. Po wyjściu z pętli wewnętrznej komputer napotka instrukcję warunkową, która bada stan zmiennej test. Jeśli test zawiera wartość różną od zera, to znaczy, że żaden z dzielników d nie dzielił liczby p. Skoro tak, to p jest pierwsze i wyświetlamy je, po czym pętla zewnętrzna wykonuje następny obieg i wyznacza kolejną liczbę p do testu.

Zapamiętaj:

continue kończy bieżący obieg pętli i rozpoczyna następny
break kończy bieżący obieg i wychodzi z pętli

Zapraszamy do następnego rozdziału.


Na początek:  podrozdziału   strony 

Zespół Przedmiotowy
Chemii-Fizyki-Informatyki

w I Liceum Ogólnokształcącym
im. Kazimierza Brodzińskiego
w Tarnowie
ul. Piłsudskiego 4
©2024 mgr Jerzy Wałaszek

Materiały tylko do użytku dydaktycznego. Ich kopiowanie i powielanie jest dozwolone
pod warunkiem podania źródła oraz niepobierania za to pieniędzy.

Pytania proszę przesyłać na adres email: i-lo@eduinf.waw.pl

Serwis wykorzystuje pliki cookies. Jeśli nie chcesz ich otrzymywać, zablokuj je w swojej przeglądarce.

Informacje dodatkowe.