C/Instrukcje sterujące

C jest językiem imperatywnym - oznacza to, że instrukcje wykonują się jedna po drugiej w takiej kolejności w jakiej są napisane. Aby móc zmienić kolejność wykonywania instrukcji potrzebne są instrukcje sterujące.

Na wstępie przypomnijmy jeszcze informację z rozdziału Operatory, że wyrażenie jest prawdziwe wtedy i tylko wtedy, gdy jest różne od zera, a fałszywe wtedy i tylko wtedy, gdy jest równe zeru.



Instrukcje warunkowe

edytuj

Użycie instrukcji if wygląda tak:

 if (wyrażenie) {
   /* blok wykonany, jeśli wyrażenie jest prawdziwe */
 }
 /* dalsze instrukcje */

Istnieje także możliwość reakcji na nieprawdziwość wyrażenia - wtedy należy zastosować słowo kluczowe else:

 if (wyrażenie) {
   /* blok wykonany, jeśli wyrażenie jest prawdziwe */
 } else {
   /* blok wykonany, jeśli wyrażenie jest nieprawdziwe */
 }
 /* dalsze instrukcje */

Przypatrzmy się bardziej "życiowemu" programowi, który porównuje ze sobą dwie liczby:

 #include <stdio.h>
 
 int main ()
 {
   int a, b;
   a = 4;
   b = 6;
   if (a==b) {
     printf ("a jest równe b\n");
   } else {
     printf ("a nie jest równe b\n");
   }
   return 0;
 }

Stosowany jest też krótszy zapis warunków logicznych, korzystający z tego, jak C rozumie prawdę i fałsz, tzn.:

  • liczba całkowita różna od zera oznacza prawdę
  • liczba całkowita równa zero oznacza fałsz.

Jeśli zmienna a jest typu integer, zamiast:

 if (a != 0) b = 1/a;

można napisać:

  if (a) b = 1/a;

a zamiast

  if (a == 0) b = 0;

można napisać:

  if (!a)  b = 0;


Czasami zamiast pisać instrukcję if możemy użyć operatora wyrażenia warunkowego (patrz Operatory).

 if (a != 0)
   b = 1/a;
 else
   b = 0;

ma dokładnie taki sam efekt jak:

 b = (a !=0) ? 1/a : 0;

Zobacz też:

switch

edytuj

Aby ograniczyć wielokrotne stosowanie instrukcji if możemy użyć switch. Jej użycie wygląda tak:

 switch (wyrażenie) {
   case wartość1: /* instrukcje, jeśli wyrażenie == wartość1 */
     break;
   case wartość2: /* instrukcje, jeśli wyrażenie == wartość2 */
     break;
   /* ... */
   default: /* instrukcje, jeśli żaden z wcześniejszych warunków nie został spełniony */
     break;
 }

Należy pamiętać o użyciu break po zakończeniu listy instrukcji następujących po case. Jeśli tego nie zrobimy, program przejdzie do wykonywania instrukcji z następnego case. Może mieć to fatalne skutki:

 #include <stdio.h>
 
 int main ()
 {
   int a, b;
   printf ("Podaj a: ");
   scanf ("%d", &a);
   printf ("Podaj b: ");
   scanf ("%d", &b);
   switch (b) {
     case  0: printf ("Nie można dzielić przez 0!\n"); /* tutaj zabrakło break! */
     default: printf ("a/b=%d\n", a/b);
   }
   return 0;
 }

A czasami może być celowym zabiegiem (tzw. "fall-through") - wówczas warto zaznaczyć to w komentarzu. Oto przykład:

 #include <stdio.h>
 
 int main ()
 {
   int a = 4;
   switch ((a%3)) {
     case  0:
       printf ("Liczba %d dzieli się przez 3\n", a);
       break;
     case -2:
     case -1:
     case  1:
     case  2:
       printf ("Liczba %d nie dzieli się przez 3\n", a);
       break;
   }
   return 0;
 }

Przeanalizujmy teraz działający przykład:

 #include <stdio.h>
 
 int main ()
 {
   unsigned int dzieci = 3, podatek=1000;
   switch (dzieci) {
      case  0: break; /* brak dzieci - czyli brak ulgi */ 
      case  1: /* ulga  2% */
        podatek = podatek - (podatek/100* 2); 
        break;
      case  2: /* ulga  5% */
        podatek = podatek - (podatek/100* 5);
        break;
      default: /* ulga 10% */
        podatek = podatek - (podatek/100*10);
        break; 
   }
   printf ("Do zapłaty: %d\n", podatek);
 }

Pętle

edytuj

Często zdarza się, że nasz program musi wielokrotnie powtarzać ten sam ciąg instrukcji. Aby nie przepisywać wiele razy tego samego kodu można skorzystać z tzw. pętli. Pętla wykonuje się dopóty, dopóki prawdziwy jest warunek.

 while (warunek) {
   /* instrukcje do wykonania w pętli */
 }
 /* dalsze instrukcje */

Całą zasadę pętli zrozumiemy lepiej na jakimś działającym przykładzie. Załóżmy, że mamy obliczyć kwadraty liczb od 1 do 10. Piszemy zatem program:

 #include <stdio.h>
 
 int main ()
 {
   int a = 1;
   while (a <= 10) { /* dopóki a nie przekracza 10 */
     printf ("%d\n", a*a); /* wypisz a*a na ekran*/
     ++a; /* zwiększamy a o jeden*/
   }
   return 0;
 }

Po analizie kodu mogą nasunąć się dwa pytania:

  • Po co zwiększać wartość a o jeden? Otóż gdybyśmy nie dodali instrukcji zwiększającej a, to warunek zawsze byłby spełniony, a pętla "kręciłaby" się w nieskończoność.
  • Dlaczego warunek to "a <= 10" a nie "a!=10"? Odpowiedź jest dość prosta. Pętla sprawdza warunek przed wykonaniem kolejnego "obrotu". Dlatego też gdyby warunek brzmiał "a!=10" to dla a=10 jest on nieprawdziwy i pętla nie wykonałaby ostatniej iteracji, przez co program generowałby kwadraty liczb od 1 do 9, a nie do 10.

Od instrukcji while czasami wygodniejsza jest instrukcja for. Umożliwia ona wpisanie ustawiania zmiennej, sprawdzania warunku i inkrementowania zmiennej w jednej linijce co często zwiększa czytelność kodu.

Instrukcję for stosuje się w następujący sposób:

 for (wyrażenie1; wyrażenie2; wyrażenie3) {
   /* instrukcje do wykonania w pętli */
 }
 /* dalsze instrukcje */

Jak widać, pętla for znacznie różni się od tego typu pętli, znanych w innych językach programowania.

Opiszemy więc, co oznaczają poszczególne wyrażenia:

  • wyrażenie1 (ang. initializationStatement) - jest to instrukcja, która będzie wykonana przed pierwszym przebiegiem pętli. Zwykle jest to inicjalizacja zmiennej, która będzie służyła jako "licznik" przebiegów pętli.
  • wyrażenie2 (ang. testExpression) - jest warunkiem trwania pętli. Pętla wykonuje się tak długo, jak prawdziwy jest ten warunek.
  • wyrażenie3 (ang. updateStatement) - jest to instrukcja, która wykonywana będzie po każdym przejściu pętli ( także po ostatnim). Zamieszczone są tu instrukcje, które zwiększają licznik o odpowiednią wartość.

Jeżeli wewnątrz pętli nie ma żadnych instrukcji continue (opisanych niżej) to jest ona równoważna z:

 {
   wyrażenie1;
   while (wyrażenie2) {
     /* instrukcje do wykonania w pętli */
     wyrażenie3;
   }
 }
 /* dalsze instrukcje */

Ważną rzeczą jest tutaj to, żeby zrozumieć i zapamiętać jak tak naprawdę działa pętla for. Początkującym programistom nieznajomość tego faktu sprawia wiele problemów.

W pierwszej kolejności w pętli for wykonuje się wyrażenie1. Wykonuje się ono zawsze, nawet jeżeli warunek przebiegu pętli jest od samego początku fałszywy.

Po wykonaniu wyrażenie1 pętla for sprawdza warunek zawarty w wyrażenie2, jeżeli jest on prawdziwy ( inny niż zero), to wykonywana jest treść pętli for, czyli najczęściej to co znajduje się między klamrami, lub gdy ich nie ma, następna pojedyncza instrukcja. W szczególności musimy pamiętać, że sam średnik też jest instrukcją - instrukcją pustą.

Gdy już zostanie wykonana treść pętli for, następuje wykonanie wyrażenie3. Należy zapamiętać, że wyrażenie3 zostanie wykonane, nawet jeżeli był to już ostatni obieg pętli. Poniższe 4 przykłady pętli for w rezultacie dadzą ten sam wynik. Wypiszą na ekran liczby od 1 do 10.


 
 for(i=1; i<=10; ++i){
  printf("%d", i);
 }

 for(i=1; i<=10; ++i){
  printf("%d", i);
}

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

 
 // 4 przykład 
 for(i=1; i<10; printf("i = %d", i++ ) ); 
 printf(" i = %d", i ); // wyrażenie3 i++ zostanie wykonane, nawet jeżeli był to już ostatni obieg pętli

Dwa pierwsze przykłady korzystają z własności struktury blokowej, kolejny przykład jest już bardziej wyrafinowany i korzysta z tego, że jako wyrażenie3 może zostać podane dowolne bardziej skomplikowane wyrażenie, zawierające w sobie inne podwyrażenia. A oto kolejny program, który najpierw wyświetla liczby w kolejności rosnącej, a następnie wraca.

 #include <stdio.h>
 int main()
 {
  int i;
  for(i=1; i<=5; ++i){   
    printf("%d", i);
    }  

  for( ; i>=1; --i){
    printf("%d", i);
    }
 
  return 0;
 }

Po analizie powyższego kodu, początkujący programista może stwierdzić, że pętla wypisze 123454321. Stanie się natomiast inaczej. Wynikiem działania powyższego programu będzie ciąg cyfr 12345654321. Pierwsza pętla wypisze cyfry "12345", lecz po ostatnim swoim obiegu pętla for (tak jak zwykle) zinkrementuje zmienną i. Gdy druga pętla przystąpi do pracy, zacznie ona odliczać począwszy od liczby i=6, a nie 5. By spowodować wyświetlanie liczb od 1 do 5 i z powrotem wystarczy gdzieś między ostatnim obiegiem pierwszej pętli for a pierwszym obiegiem drugiej pętli for zmniejszyć wartość zmiennej i o 1.

Niech podsumowaniem będzie jakiś działający fragment kodu, który może obliczać wartości kwadratów liczb od 1 do 10.

   
 #include <stdio.h>
 
 int main ()
 {
   int a;
   for (a=1; a<=10; ++a) {
     printf ("%d\n", a*a);
   }
   return 0;
 }

do..while

edytuj

Pętle while i for mają jeden zasadniczy mankament - może się zdarzyć, że nie wykonają się ani razu. Aby mieć pewność, że nasza pętla będzie miała co najmniej jeden przebieg musimy zastosować pętlę do while. Wygląda ona następująco:

 do {
   /* instrukcje do wykonania w pętli */
 } while (warunek);
 /* dalsze instrukcje */

Zasadniczą różnicą pętli do while jest fakt, iż sprawdza ona warunek pod koniec swojego przebiegu. To właśnie ta cecha decyduje o tym, że pętla wykona się co najmniej raz. A teraz przykład działającego kodu, który tym razem będzie obliczał trzecią potęgę liczb od 1 do 10.

 #include <stdio.h>
 
 int main ()
 {
   int a = 1;
   do {
     printf ("%d\n", a*a*a);
     ++a;
   } while (a <= 10);
   return 0;
 }

Może się to wydać zaskakujące, ale również przy tej pętli zamiast bloku instrukcji można zastosować pojedynczą instrukcję, np.:

 #include <stdio.h>
 
 int main ()
 {
   int a = 1;
   do printf ("%d\n", a*a*a); while (++a <= 10);
   return 0;
 }

Instrukcja break pozwala na opuszczenie wykonywania pętli w dowolnym momencie. Przykład użycia:

 int a;
 for (a=1 ; a != 9 ; ++a) {
   if (a == 5) break;
   printf ("%d\n", a);
 }

Program wykona tylko 4 przebiegi pętli, gdyż przy 5 przebiegu instrukcja break spowoduje wyjście z pętli.

Break i pętle nieskończone

edytuj

W przypadku pętli for nie trzeba podawać warunku. W takim przypadku kompilator przyjmie, że warunek jest stale spełniony. Oznacza to, że poniższe pętle są równoważne:

 for (;;) { /* ... */ }
 for (;1;) { /* ... */ }
 for (a;a;a) { /* ... */} /*gdzie a jest dowolną liczba rzeczywistą różną od 0*/
 while (1) { /* ... */ }
 do { /* ... */ } while (1);

Takie pętle nazywamy pętlami nieskończonymi, które przerwać może jedynie instrukcja break[1](z racji tego, że warunek pętli zawsze jest prawdziwy) [2].

Wszystkie fragmenty kodu działają identycznie:

 int i = 0;
 for (;i!=5;++i) {
   /* kod ... */
 }

 int i = 0;
 for (;;++i) {
   if (i == 5) break;
 }

 int i = 0;
 for (;;) {
   if (i == 5) break;
   ++i;
 }

continue

edytuj

W przeciwieństwie do break, która przerywa wykonywanie pętli instrukcja continue powoduje przejście do następnej iteracji, o ile tylko warunek pętli jest spełniony. Przykład:

 int i;
 for (i = 0 ; i < 100 ; ++i) {
   printf ("Poczatek\n");
   if (i > 40) continue ;
   printf ("Koniec\n");
 }

Dla wartości i większej od 40 nie będzie wyświetlany komunikat "Koniec". Pętla wykona pełne 100 przejść.


Oto praktyczny przykład użycia tej instrukcji:

 #include <stdio.h>
 int main()
 {
   int i;
   for (i = 1 ; i <= 50 ; ++i) {
     if (i%4 == 0) continue ;
     printf ("%d, ", i);
   }
   return 0;
 }

Powyższy program generuje liczby z zakresu od 1 do 50, które nie są podzielne przez 4.

Istnieje także instrukcja, która dokonuje skoku do dowolnego miejsca programu, oznaczonego tzw. etykietą.

 etykieta:
 /* instrukcje */
 goto etykieta;

Uwaga!: kompilator GCC w wersji 4.0 i wyższych jest bardzo uczulony na etykiety zamieszczone przed nawiasem klamrowym, zamykającym blok instrukcji. Innymi słowy: niedopuszczalne jest umieszczanie etykiety zaraz przed klamrą, która kończy blok instrukcji, zawartych np. w pętli for. Można natomiast stosować etykietę przed klamrą kończącą daną funkcję.

Przykład uzasadnionego użycia:

 int i,j;
 for (i = 0; i < 10; ++i) {
   for (j = i; j < i+10; ++j) {
     if (i + j % 21 == 0) goto koniec;
   }
 }
 koniec:
 /* dalsza czesc programu */

Zobacz również : obsługa nielokalnych skoków

Natychmiastowe kończenie programu - funkcja exit

edytuj

Program może zostać w każdej chwili zakończony - do tego właśnie celu służy funkcja exit. Używamy jej następująco:

 exit (kod_wyjścia);

Liczba całkowita kod_wyjścia jest przekazywana do procesu macierzystego, dzięki czemu dostaje on informację, czy program w którym wywołaliśmy tą funkcję zakończył się poprawnie lub czy się tak nie stało. Kody wyjścia są nieustandaryzowane i żeby program był w pełni przenośny należy stosować makra EXIT_SUCCESS i EXIT_FAILURE, choć na wielu systemach kod 0 oznacza poprawne zakończenie, a kod różny od 0 błędne. W każdym przypadku, jeżeli nasz program potrafi generować wiele różnych kodów, warto je wszystkie udokumentować w ew. dokumentacji. Są one też czasem pomocne przy wykrywaniu błędów.

Odwijanie pętli

edytuj

Odwijanie pętli ( ang. Loop unrolling) jest metodą optymalizacji oprogramowania powodującą przyspieszenie wykonania pętli. Polega na zmianie kodu programu przez kilkukrotne skopiowanie zawartości pętli i odpowiednie zmniejszenie liczby powtórzeń. Dzięki temu eliminuje się niepotrzebne sprawdzanie warunku zakończenia.

Przykładowo pętla wykonująca 100 razy funkcję delete(x), również 100 razy sprawdzi warunek zakończenia x<100:

 for (int x = 0; x < 100; x++)
 {
     delete(x);
 }

Można jednak nieznacznie wydłużyć kod powtarzając instrukcję delete(x) oraz zmniejszyć liczbę przejść przez pętlę, przez co wykona się kilkukrotnie mniej sprawdzeń x<100:

 for (int x = 0; x < 100; x += 5)
 {
     delete(x);
     delete(x+1);
     delete(x+2);
     delete(x+3);
     delete(x+4);
 }

Użycie odwijania pętli zwiększa objętość programu, dlatego potrzebne jest znalezienie optymalnej liczby powtórzeń. Stosowanie odwijania pętli w programowaniu nie jest jednak konieczne ponieważ większość kompilatorów sama znajduje optymalną wersję odwinięcia i umieszcza ją w kodzie wynikowym.

  • W języku C++ można deklarować zmienne w nagłówku pętli "for" w następujący sposób: for(int i=0; i<10; ++i) (więcej informacji w C++/Zmienne)
  • [C/Operatory#Operator_wyrażenia_warunkowego| Operator_wyrażenia_warunkowego ?]


Przypisy

  1. Tak naprawdę podobną operacje, możemy wykonać za pomocą polecenia goto. W praktyce jednak stosuje się zasadę, że break stosuje się do przerwania działania pętli i wyjścia z niej, goto stosuje się natomiast wtedy, kiedy chce się wydostać z kilku zagnieżdżonych pętli za jednym zamachem. Do przerwania pracy pętli mogą nam jeszcze posłużyć polecenia exit() lub return, ale wówczas zakończymy nie tylko działanie pętli, ale i całego programu/funkcji.
  2. Żartobliwie można powiedzieć, że stosując pętlę nieskończoną to najlepiej korzystać z pętli for(;;){}, gdyż wymaga ona napisania najmniejszej liczby znaków w porównaniu do innych konstrukcji.