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

Instrukcje warunkowe

SPIS TREŚCI
Podrozdziały

Wyrażenia logiczne

Wartością w logice jest prawda lub fałsz. W języku C prawdą jest dowolna wartość różna od zera, a fałszem jest wartość zero. Dzięki takiej definicji można traktować wyrażenia arytmetyczne jak logiczne. Na przykład wyrażenie --a jest prawdziwe, jeśli po zmniejszeniu zmiennej a o 1 zmienna ta wciąć zawiera wartość różną od zera.

Jednakże częściej będziesz miał do czynienia z wyrażeniami, w których są stosowane operatory porównań.

Uruchom poniższe programy:

/*
 Operatory porównań
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 24.09.2016
*/

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

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

  printf("%d\n\n",3 > 2);
  return 0;
}
/*
 Operatory porównań
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 24.09.2016
*/

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

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

  printf("%d\n\n",2 > 3);
  return 0;
}

W pierwszym przypadku otrzymujemy wynik 1, ponieważ wyrażenie 3 > 2 jest logicznie prawdziwe. Operator > jest operatorem logicznym. Jeśli relacja większości jest spełniona, to daje on wynik 1. A jeśli nie, to daje wynik 0, jak w programie drugim.

Pełna składnia operatora > wygląda następująco:
wyrażenie1  > wyrażenie2

Jeśli wartość wyrażenia1 jest faktycznie większa od wartości wyrażenia2, to wartością całego wyrażenia jest 1 (logiczna prawda). W przeciwnym razie całe wyrażenie przyjmuje wartość 0.

Każde z wyrażeń może być dowolnym wyrażeniem arytmetycznym lub logicznym, co pozwala z kolei testować złożone warunki. W poniższej tabeli zebrałem często używane operatory porównań:
== równość a == b Równe 1, gdy a i b mają tą samą wartość. Inaczej równe 0.
!= różność a != b Równe 1, gdy wartości a i b różnią się. Inaczej równe 0.
> większość a > b Równe 1, gdy a jest większe od b. Inaczej równe 0.
< mniejszość a < b Równe 1, gdy a jest mniejsze od b. Inaczej równe 0.
>= większość lub równość a >= b Równe 1, gdy a jest większe lub równe b. Inaczej równe 0.
<= mniejszość lub równość a <= b Równe 1, gdy a jest mniejsze lub równe b. Inaczej równe 0.

Poniższy program pokazuje przykładowe wykorzystanie wyrażeń logicznych. Wyświetla on kolejne cyfry szesnastkowe (później pokażę, jak zrobić to bardziej efektywnie):
/*
 Wyrażenia logiczne
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 24.09.2016
*/

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

int main()
{
  int h = 0;

  setlocale(LC_ALL,"");

  printf("%c ",48 + h + 7 * (h > 9)); h++;
  printf("%c ",48 + h + 7 * (h > 9)); h++;
  printf("%c ",48 + h + 7 * (h > 9)); h++;
  printf("%c ",48 + h + 7 * (h > 9)); h++;
  printf("%c ",48 + h + 7 * (h > 9)); h++;
  printf("%c ",48 + h + 7 * (h > 9)); h++;
  printf("%c ",48 + h + 7 * (h > 9)); h++;
  printf("%c ",48 + h + 7 * (h > 9)); h++;
  printf("%c ",48 + h + 7 * (h > 9)); h++;
  printf("%c ",48 + h + 7 * (h > 9)); h++;
  printf("%c ",48 + h + 7 * (h > 9)); h++;
  printf("%c ",48 + h + 7 * (h > 9)); h++;
  printf("%c ",48 + h + 7 * (h > 9)); h++;
  printf("%c ",48 + h + 7 * (h > 9)); h++;
  printf("%c ",48 + h + 7 * (h > 9)); h++;
  printf("%c\n\n",48 + h + 7 * (h > 9));

  return 0;
}

Zwróć uwagę na instrukcję przypisania:
int h = 0;

Jest to często używany skrót w programach w języku C. Tutaj jednocześnie definiujemy zmienną h oraz nadajemy jej wartość początkową.

W jednym wierszu możesz umieścić więcej niż jedną instrukcję. Wystarczy, że rozdzielisz je średnikiem.

Jak działa program? Wyświetla znak, którego kod jest wartością wyrażenia: 48 + h + 7 * (h > 9), po czym zmienna h jest zwiększana o 1. Dla h = 0...9 wyrażenie logiczne w nawiasie przyjmie wartość 0. Otrzymamy zatem kody 48 + h, co daje kody cyfr od 0 do 9. Jednak, gdy h osiągnie wartości 10...15, to wyrażenie logiczne (h > 9) będzie miało wartość 1. W efekcie otrzymamy kody liter od A do F.


Na początek:  podrozdziału   strony 

Instrukcja warunkowa if

W języku C istnieje specjalna instrukcja, dzięki której komputer potrafi podejmować decyzje, czyli staje się czymś więcej niż durnym liczydłem. Instrukcję tę nazywamy instrukcją warunkową if (ang. if = jeśli). Jest to jedna z najbardziej podstawowych instrukcji i trudno raczej spotkać poważniejszy program, w którym by jej nie było. Ucząc się instrukcji w języku C (na szczęście nie jest ich dużo), musisz opanować ich składnię, czyli właściwy sposób stosowania. Instrukcja if może wystąpić w dwóch podstawowych formach:
if(wyrażenie) instrukcja;

lub

if(wyrażenie) instrukcja1; else instrukcja2;

Na początek zajmijmy się pierwszą formą instrukcji if.

if(wyrażenie) instrukcja;

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

Komputer oblicza najpierw wartość wyrażenia, które podasz w nawiasach za instrukcją if. Jeśli w wyniku otrzyma wartość różną od zera (jak pamiętasz, jest to równoważne logicznej prawdzie), to wykona podaną instrukcję. Jeśli wartością wyrażenia będzie zero (logiczny fałsz), to instrukcja nie zostanie wykonana (komputer po prostu ją ominie). Tyle, albo aż tyle.

Uruchom program:

/*
 Instrukcja warunkowa
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 24.09.2016
*/

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

int main()
{
  float x = -12.7; // tutaj wpisz wartość
  float absx = x;

  setlocale(LC_ALL,"");

  if(x < 0) absx = -x;
  printf("abs(%f) = %f\n\n",x,absx);
  return 0;
}

Program tworzy dwie zmienne typu float: x oraz absx. W zmiennej x umieszcza wartość podaną przez użytkownika. Przepisuje tą wartość do zmiennej absx. Następnie sprawdza, czy x jest mniejsze od 0. Jeśli tak, to w absx umieszcza x ze znakiem przeciwnym. W efekcie w zmiennej absx zostaje utworzona wartość bezwzględna z x.

Uruchom program:

/*
 Instrukcja warunkowa
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 24.09.2016
*/

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

int main()
{
  float a = 1.2; // tutaj wpisz wartości
  float b = 3.1;
  float c = 2.9;
  float x = a; // największa liczba

  setlocale(LC_ALL,"");

  if(b > x) x = b;
  if(c > x) x = c;
  printf("Z liczb %6.2f %6.2f %6.2f największą jest %6.2f\n\n",a,b,c,x);

  return 0;
}

W programie trzem zmiennym a, b i c nadaje się wartości. Następnie w zmiennej x wyznacza się największą z nich: do x zostaje zapisana wartość zmiennej a. Jeśli b ma wartość większą od x, to b trafi do x. Podobnie, jeśli c ma wartość większą od x, to c trafi do x. Zasadę tę możesz rozszerzyć na dowolną liczbę zmiennych. W efekcie program znajdzie wśród nich największą wartość.


W składni if występuje tylko jedna instrukcja. Co zrobić, jeśli chcemy warunkowo wykonywać więcej niż jedną instrukcję? Jeśli kusi cię coś takiego:

if(warunek) instrukcja1; instrukcja2; instrukcja3;...

to niestety muszę cię zmartwić. Warunkowo będzie wykonana tylko instrukcja1. Wszystkie pozostałe zostaną wykonane bezwarunkowo, tzn. zawsze, bez względu na wartość warunku:

if(warunek) instrukcja1; instrukcja2; instrukcja3;...

Tutaj musisz zastosować twór, który nosi nazwę instrukcji blokowej. Na czym to polega. Otóż w języku C każdą instrukcję prostą można zastąpić dowolnym ciągiem instrukcji umieszczonych w klamrach { }. Właśnie taki ciąg instrukcji nazywamy instrukcją blokową. Z instrukcją blokową składnia if wygląda następująco:

if(warunek)
{
    instrukcja_1;
    instrukcja_2;
    ...
    instrukcja_n;
}

W tej nowej postaci działanie jest następujące:

Komputer oblicza wartość warunku. Jeśli otrzyma wynik różny od zera, to wykona kolejno instrukcje w klamerkach. Jeśli wynikiem będzie zero, to pominie instrukcje w klamerkach.

Istnieje kilka szkół zapisywania klamerek w języku C. Ja preferuję przejrzystość. Dlatego klamerkę otwierającą umieszczam zawsze w nowym wierszu w tej samej kolumnie, w której rozpoczyna się instrukcja if. Ułatwia mi to później analizę bardziej złożonych programów.

Zwróć uwagę, że za klamerką zamykającą nie wstawiasz już średnika. Jest to spowodowane tym, że instrukcja prosta w języku C zawsze kończy się średnikiem:

instrukcja;

A instrukcja blokowa, czyli ciąg instrukcji w klamerkach, zastępuje w całości instrukcję prostą wraz z zamykającym ją średnikiem. Jeśli jednak wstawisz za klamerką zamykającą średnik, to nic wielkiego się nie stanie (w tym konkretnym przypadku). Zostanie to potraktowane jako instrukcja pusta, czyli po prostu brak jakiegokolwiek działania. Takich średników możesz sobie wstawiać dowolną ilość. Pytanie tylko po co? Staraj się jednak nie kombinować, lecz trzymać dokładnie składni instrukcji, a unikniesz wielu kłopotów.


Wykorzystajmy nową wiedzę i napiszmy program, który porządkuje dwie liczby a i b tak, że bez względu na ich początkową zawartość otrzymasz w a liczbę mniejszą, a w b większą (jeśli są różne). Będzie tutaj potrzebna zamiana zawartości dwóch zmiennych.

Operacja ta wymaga trzech kroków oraz dodatkowej zmiennej pomocniczej, którą oznaczmy jako x.

X = A; // Zapamiętujemy zawartość zmiennej A w X
A = B; // Do A przepisujemy zawartość zmiennej B
B = X; // Do B przepisujemy zapamiętaną w X zawartość zmiennej A

W efekcie wykonania tych trzech przypisań zawartość zmiennych a i b zostanie wymieniona: to co było w zmiennej a trafi do b i na odwrót. Zapamiętaj tę
Uruchom program:

/*
 Instrukcja warunkowa
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 24.09.2016
*/

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

int main()
{
  float a = 7.2; // tutaj wpisz wartości
  float b = 3.1;
  float x;
  setlocale(LC_ALL,"");

  printf("Przed: %6.2f %6.2f\n",a,b);
  if(a > b)
  {
    x = a; a = b; b = x; // Zamieniamy
  }
  printf("Po : %6.2f %6.2f\n",a,b);

  return 0;
}

Program przypisuje wartości dwóm zmiennym a i b i wypisuje je. Następnie sprawdza, czy zmienna a jest większa od zmiennej b. Jeśli tak, to zamienia zawartości tych zmiennych ze sobą za pomocą instrukcji blokowej. Na koniec ponownie wypisuje ich wartości po instrukcji warunkowej.

if(wyrażenie) instrukcja1; else instrukcja2;

Druga forma instrukcji if działa następująco:

Komputer najpierw oblicza wartość wyrażenia. Jeśli jest ona różna od zera (w języku C oznacza to logiczną prawdę), to zostanie wykonana instrukcja1, a instrukcja2 będzie pominięta. Jeśli wyrażenie ma wartość 0 (logiczny fałsz), to zostanie pominięta instrukcja1, a wykonana instrukcja2. Czyli z dwóch instrukcji zawsze wykonywana jest tylko jedna: instrukcja1, gdy wyrażenie jest prawdziwe; instrukcja2, gdy wyrażenie jest fałszywe.

Uruchom program:

/*
 Instrukcja warunkowa
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 24.09.2016
*/

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

int main()
{
  int a = 7; // tutaj wpisz wartość

  setlocale(LC_ALL,"");

  if(a % 2) printf("%d jest nieparzyste\n",a);
  else      printf("%d jest parzyste\n",a);

 return 0;
}

Program sprawdza parzystość liczby w zmiennej a. Liczba jest parzysta, jeśli dzieli się przez 2, czyli gdy reszta z dzielenia przez dwa jest równa 0. W instrukcji if dzielimy zmienną a przez 2. Jeśli resztą będzie 1, to a jest nieparzyste i zostanie wywołana pierwsza funkcja printf, która to wypisze. jeśli otrzymamy resztę 0, to będzie wywołana druga funkcja printf.


Nic nie stoi na przeszkodzie, aby w instrukcji if instrukcją była kolejna instrukcja if. Szczególnie użyteczne jest to po else. Powstaje wtedy dosyć ciekawa konstrukcja programowa:

if(wyrażenie_1) instrukcja_1;
else if(wyrażenie_2) instrukcja_2;
else if(wyrażenie_3) instrukcja_3;
...
else instrukcja_n;

Wygląda to trochę tajemniczo, lecz zasada działania jest prosta:

Komputer oblicza wyrażenie_1. Jeśli jest prawdziwe (różne od zera), to wykonuje instrukcję_1, a resztę pomija. W przeciwnym razie zostaje obliczone wyrażenie_2. Jeśli jest prawdziwe, to będzie wykonana instrukcja_2, a resztę instrukcji komputer pominie. Inaczej sytuacja się powtarza dla każdego kolejnego członu else if. Komputer wylicza w tym członie wyrażenie. Jeśli jest prawdziwe, to wykonuje instrukcję członu. Jeśli nie, przechodzi do kolejnego członu. Jeśli żadne z wyrażeń nie będzie prawdziwe, to zostanie wykonana instrukcja po ostatnim else, czyli instrukcja_n (człon ten można pominąć).

Uruchom program.

/*
 Instrukcja warunkowa
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 24.09.2016
*/

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

int main()
{
  int wiek = 45; // wpisz swój wiek

  setlocale(LC_ALL,"");

  printf("Witaj. Masz lat %d\n", wiek);
  if(wiek < 10) printf("Jesteś dzieckiem\n");
  else if(wiek < 18) printf("Jesteś nastolatkiem\n");
  else if(wiek < 66) printf("Jesteś dorosłym\n");
  else if(wiek < 100) printf("Jesteś seniorem\n");
  else printf("Gratulacje. Piękny wiek!\n");

  return 0;
}

Program na podstawie zawartości zmiennej wiek wypisuje różne teksty (kobiety będą musiały zmienić nieco treść napisów z uwagi na seksistowski charakter języka polskiego).


Pojedyncze instrukcje proste zawsze możesz zastąpić instrukcją blokową. Wtedy składnia if będzie wyglądała następująco:

if(wyrażenie)
{
    instrukcja_p1; // instrukcje wykonywane, gdy
    instrukcja_p2; // warunek jest prawdziwy
    ...
    instrukcja_pn;
}
else
{
    instrukcja_f1; // instrukcje wykonywane, gdy
    instrukcja_f2; // warunek jest fałszywy
    ...
    instrukcja_fn;
}

Zwróć uwagę na brak średników za klamerkami zamykającymi blok. Pisałem o tym wcześniej. Tutaj umieszczenie średnika za pierwszą instrukcją blokową spowoduje powstanie błędu w programie. Postaraj się wyjaśnić, dlaczego ten błąd powstaje.

Instrukcja działa następująco:

Komputer wylicza wartość wyrażenia w nawiasach za instrukcją if. Jeśli otrzyma wynik różny od zera, to wykona po kolei instrukcje_p, a pominie blok instrukcji po else. Jeśli wyrażenie ma wartość 0, to zostaną pominięte instrukcje_p w bloku pierwszym, a będą wykonane po kolei instrukcje_f w bloku za else.


Na początek:  podrozdziału   strony 

Porównywanie liczb

W programowaniu mogą zdarzyć się sytuację, które często zaskakują programistę o niezbyt kompletnej wiedzy. To dlatego tak ważne jest posiadanie dobrej podstawy teoretycznej, czyli czegoś, o czym większość młodych ludzi zapomina.

Zakres

Zakres jest to zbiór wartości, które w danym typie liczb można przedstawiać, reprezentować. Każdy typ posiada w pamięci określoną realizację za pomocą odpowiedniej liczby bitów (bajtów). Ile to dokładnie będzie, musisz poszukać w instrukcji kompilatora, dla którego piszesz programy. Jeśli system docelowy dysponuje wyświetlaczem, to możesz na nim wyświetlić rozmiar każdego typu podstawowego. Poniższy program podaje rozmiary poznanych dotychczas typów danych:

/*
 Typy danych
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 25.09.2016
*/

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

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

  printf("char : %d B = %2d b\n",sizeof(char), 8 * sizeof(char));
  printf("int : %d B = %2d b\n",sizeof(int), 8 * sizeof(int));
  printf("unsigned : %d B = %2d b\n",sizeof(unsigned), 8 * sizeof(unsigned));
  printf("float : %d B = %2d b\n",sizeof(float), 8 * sizeof(float));

  return 0;
}

W programie używamy operatora sizeof. Mimo że wygląda on podobnie do wywołania funkcji, to jednak funkcją nie jest. Argumentem tego operatora jest dowolny typ danych lub zmienna. W wyniku otrzymujesz rozmiar (ang. size) tego argumentu, czyli liczbę bajtów, którą zajmuje on w pamięci komputera. W środowisku CodeBlocks otrzymasz 1 bajt dla typu char i po cztery bajty dla pozostałych typów danych. Jednakże w środowisku programowania określonego mikrokontrolera te rozmiary mogą być inne.

Przy typach całkowitych niespodzianki pojawiają się, gdy zostanie przekroczony zakres dopuszczalnych wartości. Pokażemy to na przykładzie typu char. Jest on reprezentowany w pamięci liczbą jednobajtową bez znaku (uwaga: czasami może to być liczba ze znakiem w kodzie U2, ponieważ zależy to od ustawień opcji kompilatora, zatem dla bezpieczeństwa lepiej stosować typ unsigned char, który gwarantuje wartości bez znaku w kodzie NBC). Zakres 8 bitowej liczby NBC wynosi: 0...28-1, czyli 0...255.

Uruchom poniższy program:
/*
 Typy danych
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 25.09.2016
*/

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

int main()
{
  unsigned char c = 65;

  setlocale(LC_ALL,"");

  printf("%d\n", c); c += 100; // c = 165
  printf("%d\n", c); c += 100; // c = 265?
  printf("%d\n", c);

  return 0;
}

I co się tutaj dzieje? W programie tworzymy zmienną c i nadajemy jej wartość 65 (kod litery A, jednak tutaj nie jest to istotne, ponieważ zmienną c traktujemy liczbowo). Wyświetlamy zawartość c pierwszą funkcją printf, otrzymując 65. Następnie zawartość c zwiększamy o 100. W kolejnym wierszu ponownie wyświetlamy zawartość c, otrzymując 165. Znów zwiększamy o 100 zmienną c. Gdy teraz po raz trzeci wyświetlimy zawartość tej zmiennej, to otrzymamy 9, a nie spodziewanej wartości 265. Dlaczego? Nastąpiło przekroczenie zakresu i po prostu liczba 265 nie mieści się w 8 bitach:
26510 = 1000010012

Zwróć uwagę, że liczba 265 zapisuje się na 9 bitach. Tymczasem zmienna c może zapamiętać z tego tylko 8 młodszych bitów. Gdy usuniemy ten najstarszy bit, to zostanie:
000010012 = 910

To wyjaśnia otrzymany wynik. Sytuacja taka jest typowa dla przekroczenia zakresu.

Dla kodu U2 jest podobnie, chociaż efekty mogą być inne. Możemy utworzyć zmienną typu char w kodzie U2 (czyli ze znakiem), jeśli dodamy modyfikator signed.

Uruchom program:

Przyjrzyjmy się dokładniej temu programowi. Na początku tworzymy zmienną c typu char ze znakiem, zatem przechowuje ona liczby 1 bajtowe w kodzie U2. Oczywiście pamiętasz, że zakres takich liczb wynosi -(27)...27 - 1, czyli -128...127. W zmiennej c umieszczamy wartość 65. Następnie wyświetlamy zawartość zmiennej c pierwszą funkcją printf, po czym c zwiększamy o 65. Ponownie wyświetlamy c i zamiast spodziewanej wartości 130 otrzymujemy -126. Dlaczego?

Komputer wykonał poprawnie działanie i zapisał w zmiennej c liczbę 130, lecz w kodzie NBC:
13510 = 100000102

Problem polega na tym, iż bity w zmiennej c są interpretowane wg kodu U2, a nie NBC. Policzmy zatem, jaką wartość ma ta liczba w kodzie U2:
waga -128 64 32 16 8 4 2 1
cyfra 1 0 0 0 0 0 1 0
10000111U2 = -128 + 2
10000111U2 = -126

I tajemnica rozwiązana. Po prostu liczba 130 wykracza poza dozwolony zakres liczb U2 i nie będzie poprawnie zinterpretowana. To nie jest winą komputera, że kazałeś mu wykonać operację, której wynik wykroczył poza dopuszczalny zakres wartości. Po prostu musisz w przyszłości uważać, aby do takich sytuacji nie dopuszczać.

Precyzja

Precyzja odnosi się do liczb zmiennoprzecinkowych. Przy programowaniu samych mikrokontrolerów raczej rzadko napotkasz typy zmiennoprzecinkowe, ponieważ ich obsługa wymaga sporej ilości kodu, a mikrokontrolery zwykle dysponują niewielką pamięcią na program. Jednak w twoim projekcie mikrokontroler może komunikować się z dużym komputerem, który będzie wykonywał skomplikowane obliczenia naukowe na liczbach zmiennoprzecinkowych. Dlatego powinieneś znać zagadnienia z nimi związane.

Pamiętasz oczywiście, że wartość liczby zmiennoprzecinkowej obliczamy ze wzoru:

W wartość liczby
m mantysa, ułamek
p podstawa systemu
c cecha, liczba całkowita

Dla przejrzystości będziemy się posługiwać liczbami dziesiętnymi, ale rozumiesz, że komputer stosuje tutaj liczby dwójkowe. Oczywiście w niczym to nie przeszkadza, ponieważ omawiane własności nie zależą od wyboru podstawy systemu.

Załóżmy zatem, że mantysa jest liczbą dziesiętną 0 < |m| < 10 i ma jedną cyfrę części całkowitej oraz dwie cyfry po przecinku. Wygląda to przykładowo tak:
1,23·103 = 1230
8,05·10-3 = 0,00805
-3,14·1020 = -314000000000000000000

Zwróć uwagę, że jeśli w wartościach tych liczb usuniemy początkowe lub końcowe zera, to otrzymamy cyfry mantysy, zwane cyframi znaczącymi. Mantysa określa tutaj tzw. rozdzielczość lub precyzję liczby. W tym przypadku mamy mantysę 3-cyfrową, zatem precyzja wynosi trzy cyfry znaczące. Pewnych liczb nie da się tutaj zapisać:
1234 ≈ 1,23·103

 Po prostu mantysa ma za mało cyfr, aby dokładnie takie liczby reprezentować. Z identyczną sytuacją mamy do czynienia w świecie dwójkowych liczb zmiennoprzecinkowych. Typ float w przeliczeniu na system dziesiętny potrafi dokładnie zapamiętać mniej więcej 7-8 cyfr znaczących. Jeśli liczba wymaga więcej cyfr (z pominięciem zer początkowych i końcowych), to nie zostanie dokładnie zapamiętana.

Uruchom poniższy program:
/*
 Typy danych
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 25.09.2016
*/

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

int main()
{
  float x = 123.456789;

  setlocale(LC_ALL,"");

  printf("%f\n",x);

  return 0;
}

Jako wynik otrzymujesz liczbę:
123.456787

Ostatnia dziewiątka gdzieś się zapodziała. To nie jest błąd. Po prostu mantysa w typie float potrafi dokładnie zapamiętać do 8 cyfr dziesiętnych (lepiej ufaj 7 cyfrom). Liczby wymagające więcej cyfr zostaną zaokrąglone. Jest to cecha charakterystyczna systemów zmiennoprzecinkowych. Nazywamy ją precyzją. Czyli typ float posiada precyzję 7-8 cyfr.  Nie nadaje się zatem do zbyt precyzyjnych obliczeń.

Pewnych liczb nie daje się w ogóle zapisać dokładnie w zmiennopozycyjnym systemie dwójkowym. Typowym przykładem jest ułamek jedna dziesiąta:

Uruchom program:
/*
 Typy danych
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 25.09.2016
*/

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

int main()
{
  float x = 0.1;

  setlocale(LC_ALL,"");

  printf("%16.14f\n",x);

  return 0;
}

W wyniku otrzymujesz:
0.10000000149012

Od pewnego miejsca po przecinku zaczynają się śmieci. Dlaczego? Podobnie jest z ułamkami 1/3, 1/7 i 1/9 w systemie dziesiętnym. Są to nieskończone ułamki okresowe. Jeśli zapiszemy je jako skończony ułamek dziesiętny, to otrzymamy tylko ich przybliżenia:

Ułamek 1/10 rozwija się w systemie dwójkowym na nieskończony ułamek okresowy:

Ponieważ mantysa posiada skończoną liczbę bitów, to ułamek zostaje zaokrąglony i dostajemy to, co widać w programie.

Z powyższych informacji wynika ważny wniosek: obliczenia na liczbach zmiennoprzecinkowych są obliczeniami przybliżonymi, a wyniki otrzymujemy tylko z pewną dokładnością. W informatyce istnieje cały dział analizy numerycznej, który zajmuje się oszacowaniami błędów obliczeń komputerowych. Nie będziemy w to zagadnienie aż tak głęboko wchodzić, lecz podamy kilka prostych wskazówek, jak sobie radzić z liczbami zmiennoprzecinkowymi w programach.

Problemy mogą pojawić się przy porównywaniu wyników obliczeń na liczbach zmiennoprzecinkowych. Uruchom poniższy program:
/*
 Typy danych
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 25.09.2016
*/

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

int main()
{
  float x = 0;

  setlocale(LC_ALL,"");

  x += 0.1; // x = 0.1
  x += 0.1; // x = 0.2
  x += 0.1; // x = 0.3
  x += 0.1; // x = 0.4
  x += 0.1; // x = 0.5
  x += 0.1; // x = 0.6
  x += 0.1; // x = 0.7
  x += 0.1; // x = 0.8
  x += 0.1; // x = 0.9
  x += 0.1; // x = 1?
  if(x == 1.0) printf("WYNIK DOBRY\n");
  else printf("WYNIK BŁĘDNY\n");
  printf("%16.14f",x);

  return 0;
}

W programie tworzymy zmienną x typu float i umieszczamy w niej zero. Następnie stopniowo zwiększamy jej zawartość co 1/10 aż do momentu, gdy powinna zawierać wartość 1. Sprawdzamy ten fakt za pomocą instrukcji if. Jeśli w x będzie 1, to program wyświetli napis WYNIK DOBRY. A jeśli nie, to pojawi się napis WYNIK BŁĘDNY. Na końcu wyświetlamy zawartość x z 14 cyframi po przecinku. Po uruchomieniu otrzymasz wynik:
WYNIK BŁĘDNY
1.00000011920929

Dlaczego tak się stało? Powstały błędy obliczeniowe, ponieważ dodawaliśmy do x przybliżone wartości ułamka 0,1. W efekcie wynik jest nieco większy niż 1. Zauważ, że nie jest to jakaś straszna różnica. Występuje dopiero na siódmym miejscu po przecinku. Niestety. komputer porównuje to z wartością dokładną 1,0 i wychodzi mu, że są to wartości różne, co jest zgodne z prawdą.

Zapamiętaj:

Liczb zmiennoprzecinkowych nie wolno przyrównywać do siebie, ponieważ błędy obliczeniowe mogą spowodować, iż spodziewana równość nie wystąpi. Zamiast tego sprawdzamy, czy różnica (zwana z greckiego epsylon) dwóch porównywanych liczb jest dostatecznie mała. Jeśli tak, to przyjmujemy, że obie liczby są sobie równe. Jeśli nie, to liczby są różne.

Zamiast:

Stosujemy

Wartość epsylon  należy dobrać wg potrzeb, jednakże nie za małe!. Na przykład przyjmujemy, że jest to liczba równa 0,00001.

We wzorze występuje wartość bezwzględna, ponieważ różnica może być dodatnia lub ujemna, a nas interesuje jedynie odległość tych dwóch liczb od siebie na osi liczbowej.

Uruchom zmieniony program:
/*
 Typy danych
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 25.09.2016
*/

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

int main()
{
  float x = 0;

  setlocale(LC_ALL,"");

  x += 0.1; // x = 0.1
  x += 0.1; // x = 0.2
  x += 0.1; // x = 0.3
  x += 0.1; // x = 0.4
  x += 0.1; // x = 0.5
  x += 0.1; // x = 0.6
  x += 0.1; // x = 0.7
  x += 0.1; // x = 0.8
  x += 0.1; // x = 0.9
  x += 0.1; // x = 1?
  if(fabs(x - 1.0) < 0.00001) printf("WYNIK DOBRY\n");
  else printf("WYNIK BŁĘDNY\n");
  printf("%16.14f",x);

  return 0;
}

Wartość bezwzględną dla liczb zmiennoprzecinkowych oblicza funkcja fabs(), która jest zdefiniowana w pliku nagłówkowym math.h. Po tych modyfikacjach program działa poprawnie.


Na początek:  podrozdziału   strony 

Funkcje logiczne

W logice funkcje logiczne są odpowiednikami działań w arytmetyce. Umożliwiają wykonywanie rachunków logicznych. Cechą charakterystyczną funkcji logicznych w języku C jest to, iż traktują one swoje argumenty jak wartości logiczne. Przypominam, wartość logiczna prawdy P w języku C odpowiada każdej wartości różnej od 0. Prawdziwymi logicznie są zatem wartości: 1, 3, 1765, -16, 2.77, 0.000001 itd. Wartość logiczna fałszu F jest wartością równą dokładnie zero. Wynikiem działania funkcji logicznej będzie 1 dla prawdy i 0 dla fałszu. Funkcje logiczne możesz używać w wyrażeniach arytmetycznych podobnie jak operatory porównań. Przejdźmy teraz do konkretów.

Negacja !

Negacja, zaprzeczenie logiczne, jest funkcją jednoargumentową, a jej wartość jest odwrotna logicznie do wartości argumentu. W języku C negację zapisujemy za pomocą wykrzyknika:

a !a
F 1
P 0

W powyższej tabelce a jest dowolnym wyrażeniem. Zapis !a czytamy nie a lub a równe zero (ponieważ prawdę otrzymamy, gdy a jest zerowe). F oznacza fałsz, czyli wartość a równą 0. P oznacza prawdę, czyli wartość a różną od zera.

Uruchom program:

/*
 Funkcje logiczne
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 26.09.2016
*/

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

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

  printf("Czy 2 + 2 = 4 ? ");
  if(!(2 + 2 != 4)) printf("Nie!!!\n");
  else printf("Tak\n");

  return 0;
}

Program jest pewnego rodzaju żartem. Sprawdzamy w nim wartość wyrażenia !(2 + 2 != 4). Wyrażenie w nawiasie jest zwykłym porównaniem: czy 4 != 4. Wynikiem tego wyrażenia jest 0, ponieważ 4 nie jest różne od 4. Teraz wynik ten jest poddawany negacji, czyli z 0 otrzymujemy 1, ponieważ tak działa funkcja !. Skoro całość ma wartość 1, to instrukcja if wywoła pierwszą funkcję printf z napisem Nie!!!. Jak widzisz, komputer też może podawać bezsensowne wyniki, jeśli program mu tak każe zrobić. To programista ma czuwać nad poprawnością swoich programów.

Teraz na poważnie. Funkcja negacji najczęściej jest stosowana do sprawdzania osiągnięcia wartości 0. Na przykład wyrażenie !(--x) jest prawdziwe, gdy w wyniku zmniejszenia o 1 zmienna x osiągnęła wartość 0. Zajmiemy się tym bliżej przy pętlach.

Alternatywa ||

Alternatywa, suma logiczna jest funkcją wieloargumentową. W języku C oznaczamy ją dwoma znakami ||. Poniższa tabelka przedstawia wartość alternatywy dla dwóch argumentów a i b, które mogą być dowolnymi wyrażeniami arytmetycznymi lub logicznymi.

a b a || b
F F 0
F P 1
P F 1
P P 1

Zapamiętaj: wynikiem alternatywy jest 0, jeśli wszystkie jej argumenty mają wartość 0. Wynikiem jest 1, jeśli dowolny argument ma wartość różną od 0.

Alternatywę stosujemy wtedy, gdy chcemy sprawdzić, czy z kilku możliwości zaszła przynajmniej jedna.

Uruchom program:

/*
 Funkcje logiczne
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 26.09.2016
*/

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

int main()
{
  float a = 3.7; // zakres, dół
  float b = 6.2; // zakres, góra
  float x = 3.699999; // parametr

  setlocale(LC_ALL,"");

  if((x < a) || (x > b)) printf("%f poza zakresem %f...%f\n", x, a, b);
  else printf("%f w zakresie %f...%f\n", x, a, b);

  return 0;
}

Program tworzy trzy zmienne a, b i x. W zmiennych a i b zostaje zapamiętany zakres dozwolonych wartości dla x. Następnie sprawdzane jest, czy x wykracza poza zakres a...b. Będzie tak, jeśli x jest mniejsze od a lub x jest większe od b.

Warunki nie muszą być umieszczane w nawiasach, ponieważ operatory porównań posiadają wyższy priorytet od operatora alternatywy, jednak dla przejrzystości warto je zastosować.

Koniunkcja &&

Koniunkcja, iloczyn logiczny jest funkcją wieloargumentową. W języku C oznaczamy ją dwoma znakami &&. W tabelce poniżej przedstawione są wartości koniunkcji dla dwóch argumentów:

a b a && b
F F 0
F P 0
P F 0
P P 1

Zapamiętaj: wynikiem koniunkcji jest 1, jeśli wszystkie jej argumenty mają wartość różną od 0. Jeśli dowolny z argumentów ma wartość 0, to koniunkcja również przyjmuje wartość 0.

Koniunkcję stosujemy, jeśli chcemy sprawdzić, czy są spełnione wszystkie wymagane warunki.

Drobna przeróbka poprzedniego programu:
/*
 Funkcje logiczne
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 26.09.2016
*/

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

int main()
{
  float a = 3.7;      // zakres, dół
  float b = 6.2;      // zakres, góra
  float x = 3.699999; // parametr

  setlocale(LC_ALL,"");

  if((x >= a) && (x <= b)) printf("%f w zakresie %f...%f\n", x, a, b);
  else printf("%f poza zakresem %f...%f\n", x, a, b);

  return 0;
}

Zwarcie logiczne

W języku C wyrażenia będące argumentami funkcji logicznych są wyliczane do momentu, gdy znany jest wynik końcowy funkcji. Opcję tę zwykle można wyłączyć w ustawieniach kompilatora, lecz zwykle się tego nie robi. Na czym to polega? Wyobraźmy sobie, że obliczana jest wartość alternatywy trzech argumentów a, b i c:

a || b || c

Komputer najpierw oblicza wyrażenie a. Jeśli wyjdzie mu wartość różna od 0, to w tym momencie wiadomo już, że alternatywa będzie miała wartość 1. Dlatego wyrażenia b i c nie są obliczane. Jeśli wyrażenie a jest równe 0, to komputer wyliczy b i podobnie jeśli otrzyma wartość różną od 0, to nie będzie już liczył wyrażenia c, ponieważ zna wynik alternatywy.

W przypadku koniunkcji jest podobnie, tylko tutaj wyliczanie argumentów jest przerywane po napotkaniu wartości 0, gdyż wtedy wiadomo, że koniunkcja przyjmie wartość 0, zatem pozostałe argumenty nic już w tym wyniku nie zmienią.

Uruchom program:

/*
 Funkcje logiczne
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 26.09.2016
*/

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

int main()
{
  int a,b,c;

  setlocale(LC_ALL,"");

  a = b = c = 0;
  printf("Alternatywa: %d ",(++a)||(++b)||(++c));
  printf("Argumenty: %d %d %d\n",a,b,c);
  a = b = c = 1;
  printf("Koniunkcja : %d ",(--a)&&(--b)&&(--c));
  printf("Argumenty: %d %d %d\n",a,b,c);

  return 0;
}

W programie tworzymy trzy zmienne całkowite a, b i c.  Na początku nadajemy im wartość 0. Następnie wyświetlamy wynik alternatywy:

(++a)||(++b)||(++c)

Argumentami są wyrażenia modyfikacji zmiennych a, b i c, które zwiększają ich zawartości o 1. Jak pamiętasz, operator ++ umieszczony przed zmienną najpierw ją modyfikuje, a dopiero później zwraca wartość zmiennej, która już jest zwiększona o 1. Zatem w pierwszym wyrażeniu (++a) komputer otrzyma wartość 1. Jeśli dowolny argument alternatywy przyjmuje wartość różną od 0, to cała alternatywa otrzymuje wartość 1 (logiczna prawda w C). Zatem w tym miejscu komputer przestaje obliczać wartości dalszych argumentów alternatywy: (++b) oraz (++c). W efekcie zmienna będzie zmieniona na 1, ale zmienne b i c zachowają wartość 0, ponieważ nie były modyfikowane w alternatywie. Nastąpiło "zwarcie".

W drugiej części programu jest podobnie, tylko teraz mamy tutaj koniunkcję. Najpierw wpisujemy do wszystkich zmiennych wartość 1. Następnie wyświetlamy wynik koniunkcji:

(--a)&&(--b)&&(--c)

Komputer wylicza pierwszy argument koniunkcji: (--a). Operator -- przed zmienną najpierw zmniejsza jej zawartość o 1, a następnie daje w wyniku zmienną zmodyfikowaną. Tutaj wynikiem będzie 0. Skoro jeden z argumentów koniunkcji ma wartość 0, to cała koniunkcja również przyjmuje wartość 0. Dlatego komputer nie wylicza już pozostałych dwóch argumentów: (--b) oraz (--c). W efekcie zmodyfikowana będzie tylko zmienna a na 0, pozostałe zmienne b i c zachowają wartość 1.

Zwarcie logiczne jest istotne tylko wtedy, gdy w obrębie argumentu prowadzone są jakieś modyfikacje lub użyta zostaje funkcja, która wykonuje jakieś działania. W takich przypadkach modyfikacje mogą nie zostać wykonane, co ilustruje nasz przykładowy program. Będę na fakt zwracał uwagę w dalszej części tego artykułu.


Operatory funkcji mogą być stosowane w instrukcjach modyfikacji:

a ||= b; odpowiada operacji a = a || b;

a &&= b; odpowiada operacji a = a && b;

Na początek:  podrozdziału   strony 

Instrukcja wyboru switch

W języku C istnieje bardzo pożyteczna instrukcja switch, która umożliwia wykonywanie warunkowo wielu różnych działań w zależności od wartości wybranego wyrażenia. Jej składnia jest następująca:
switch(wyrażenie)
{
  case stała_1 : instrukcja;
                 instrukcja;
                 ...
                 break;
  case stała_1 : instrukcja;
                 instrukcja;
                 ...
                 break;
  ...
  case stała_n : instrukcja;
                 instrukcja;
                 ...
                 break;
  default: instrukcja;
           instrukcja;
           ...
           break;
}

Wygląda to dosyć skomplikowanie, lecz jest bardzo proste, gdy już zrozumiesz, jak to działa:

Komputer najpierw oblicza wartość wyrażenia podanego w nawiasach za instrukcją switch. Następnie porównuje tę wartość z kolejnymi stałymi, które są umieszczone za słówkiem case (ang. przypadek, sprawa). Jeśli stwierdzi równość, to wykonuje instrukcje zawarte za tą stałą. Instrukcje są wykonywane kolejno jedna po drugiej.  Na ich końcu powinna się znaleźć instrukcja break, która przerwie wykonywanie kodu w instrukcji switch (program zacznie wykonywać dalszą część programu za klamerką zamykającą instrukcję switch). Jeśli nie umieścisz na końcu instrukcji break, to komputer zacznie wykonywać instrukcje za kolejną stałą – czasem jest to pożądane. Jeśli żadna ze stałych za słówkiem case nie ma wartości równej wartości wyrażenia, to komputer wykona instrukcje zawarte za słowem default (ang. standardowo). Opcja ta jest zwykle umieszczana na końcu instrukcji switch, lecz nie ma takiego obowiązku. Dlatego dobrze jest również zakończyć instrukcje z default rozkazem break. Jeśli kiedyś dopiszesz nową stałą case, wykonanie default nie przejdzie do jej instrukcji. Sekcję default można pominąć. W takim razie w przypadku niezgodności wartości wyrażenia z wartościami stałych za case, nie zostaną wykonane żadne instrukcje w ramach switch.

Uruchom program:
/*
 Instrukcja wyboru
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 16.10.2016
*/

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

int main()
{
  int n = 0; // tu wpisz liczbę od 0 do 9
             // lub dowolną inną

  setlocale(LC_ALL,"");

  printf("%d to ",n);
  switch(n)
  {
    case 0 : printf("ZERO"); break;
    case 1 : printf("JEDEN"); break;
    case 2 : printf("DWA"); break;
    case 3 : printf("TRZY"); break;
    case 4 : printf("CZTERY"); break;
    case 5 : printf("PIĘĆ"); break;
    case 6 : printf("SZEŚĆ"); break;
    case 7 : printf("SIEDEM"); break;
    case 8 : printf("OSIEM"); break;
    case 9 : printf("DZIEWIĘĆ"); break;
    default: printf("NIE WIEM ILE"); break;
  }
  printf(".\n");

  return 0;
}

Instrukcja switch dla n = 0...9 wypisuje słownie wartość liczby n. Dla innych n wypisuje tekst NIE WIEM ILE. Aby zobaczyć, na czym polega przechodzenie do instrukcji innej stałej, uruchom nieco zmieniony program:
/*
 Instrukcja wyboru
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 16.10.2016
*/

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

int main()
{
  int n = 4; // tu wpisz liczbę od 0 do 9
             // lub dowolną inną

  setlocale(LC_ALL,"");

  printf("%d to ",n);
  switch(n)
  {
    case 0 : printf("ZERO"); break;
    case 1 : printf("JEDEN"); break;
    case 2 : printf("DWA"); break;
    case 3 : printf("TRZY"); break;
    case 4 : printf("CZTERY");
    case 5 : printf("PIĘĆ");
    case 6 : printf("SZEŚĆ");
    case 7 : printf("SIEDEM"); break;
    case 8 : printf("OSIEM"); break;
    case 9 : printf("DZIEWIĘĆ"); break;
    default: printf("NIE WIEM ILE"); break;
  }
  printf(".\n");

  return 0;
}

Dla n = 4 otrzymamy napis CZTERYPIEĆSZEŚĆSIEDEM, ponieważ zostaną wykonane instrukcje dla stałych 4, 5, 6 i 7.

Dla n = 5 otrzymamy napis PIEĆSZEŚĆSIEDEM, ponieważ zostaną wykonane instrukcje dla stałych 5, 6 i 7.

Dla n = 6 otrzymamy napis SZEŚĆSIEDEM, ponieważ zostaną wykonane instrukcje dla stałych 6 i 7.


Na początek:  podrozdziału   strony 

Operator warunkowy ?

Operator warunkowy ? pozwala obliczać warunkowo wyrażenia (nie myl go z instrukcją warunkową if, która wykonuje warunkowo instrukcje). Posiada on następującą składnię:
wyrażenie_1 ? wyrażenie_2 : wyrażenie_3

Działanie jest następujące:

Komputer wylicza wartość wyrażenia 1. Jeśli jest różne od zera, to oblicza wyrażenie 2 i jego wartość jest zwracana jako wynik. W przeciwnym razie wynikiem będzie wartość wyrażenia 3.

Poniższy przykład oblicza wartość bezwzględną liczby:
/*
 Operator warunkowy
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 16.10.2016
*/

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

int main()
{
  float a = -5.64;

  setlocale(LC_ALL,"");

  printf("|%f| = %f\n", a, a >= 0 ? a : -a);

  return 0;
}

Następny program oblicza wartość funkcji signum:

/*
 Operator warunkowy
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 16.10.2016
*/

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

int main()
{
  float a = -5.125;

  setlocale(LC_ALL,"");

  printf("signum(%f) = %d\n", a, a == 0 ? 0 : a > 0 ? 1 : -1);

  return 0;
}

Spróbuj samodzielnie przeanalizować działanie tego programu.

Zapraszam 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.