Decyzje: program, który wybiera właściwą drogę
Instrukcja warunkowa nie służy do „wstawiania if„. Służy do zapisania kompletnej reguły decyzyjnej: jakie przypadki istnieją, które z nich się wykluczają i co program ma zrobić w każdym z nich.
Czego powinieneś nauczyć się na tych zajęciach?
Po laboratorium powinieneś umieć nie tylko zapisać instrukcję if, ale przede wszystkim zamienić opis słowny na rozłączne przypadki, przewidzieć wybraną gałąź programu i przygotować test dla każdej istotnej sytuacji.
- interpretować wynik porównania jako fałsz albo prawdę,
- odróżniać
==od przypisania=, - stosować
if,else ifielse, - łączyć warunki operatorami
&&,||i!, - sprawdzać kolejność i kompletność gałęzi,
- dobierać przypadki zwykłe, graniczne i rozróżniające błędne rozwiązania.
Najważniejsza myśl: kod może wygenerować narzędzie AI, ale to Ty musisz wiedzieć, jakie przypadki powinien rozróżnić program. Bez tej wiedzy nie potrafisz ocenić, czy wygenerowany warunek jest kompletny.
Warunek jest pytaniem, na które program odpowiada „tak” albo „nie”
Wyrażenie warunkowe jest obliczane tak samo jak inne wyrażenia, ale interesuje nas jego znaczenie logiczne. W języku C wartość 0 oznacza fałsz, a każda wartość różna od zera – prawdę. Najczęściej warunki tworzymy za pomocą operatorów porównania.
| Operator | Znaczenie | Przykład |
== | równe | x == 0 |
!= | różne | x != y |
<, <= | mniejsze, nie większe | age >= 18 |
>, >= | większe, nie mniejsze | temperature > 0.0 |
=i==nie są zamienne. Zapisx = 5zmienia stan programu, ax == 5zadaje pytanie. Kompilator może zaakceptować przypisanie użyte w warunku, dlatego ostrzeżenia i uważne czytanie kodu są ważne.
Nie przenoś też automatycznie zapisu matematycznego do C. Nierówność a < b < c trzeba zapisać jako a < b && b < c. Pierwsza wersja w C ma inne znaczenie, ponieważ każde porównanie jest wykonywane oddzielnie.
Pierwsza instrukcja if
Instrukcja if oblicza warunek zapisany w nawiasach. Jeżeli warunek jest prawdziwy, wykonuje blok pomiędzy klamrami. Po zakończeniu bloku program idzie dalej. Same klamry nie są ozdobą – jednoznacznie pokazują, które instrukcje należą do decyzji.
1. Prześledź decyzję przed uruchomieniem
Nie uruchamiaj od razu programu. Zapisz wartości x i y po każdej instrukcji, wskaż wynik obu warunków i przewidź dokładny tekst na wyjściu. Dopiero później sprawdź odpowiedź kompilatorem.
#include <stdio.h>
int main(void) {
int x = 7;
int y = 10;
if (x < y) {
x = x + 3;
}
if (x == y) {
printf("A\n");
} else {
printf("B\n");
}
printf("%d %d\n", x, y);
return 0;
}
Odpowiedź: pierwszy warunek jest prawdziwy, więc x przyjmuje wartość 10. Drugi warunek również jest prawdziwy. Program wypisze A, a następnie 10 10.
2. Bezpieczne dzielenie
Wczytaj licznik i mianownik. Jeżeli mianownik jest równy zeru, wypisz komunikat; w przeciwnym przypadku oblicz iloraz. To przykład warunku ochronnego: najpierw sprawdzamy, czy operacja jest dozwolona.
Rozwiązanie#include <stdio.h>
int main(void) {
double numerator, denominator;
scanf("%lf %lf", &numerator, &denominator);
if (denominator == 0.0) {
printf("Nie mozna dzielic przez zero.\n");
} else {
printf("wynik: %.6f\n", numerator / denominator);
}
return 0;
}
10 4 → 2.50000010 0 → komunikat10 -4 → -2.500000else i pełny podział przypadków
else opisuje to, co ma się wydarzyć, gdy warunek poprzedzającego if jest fałszywy. Konstrukcja else if pozwala sprawdzać kolejne przypadki. Program wykonuje pierwszą pasującą gałąź, a pozostałe pomija. Z tego powodu kolejność ma znaczenie.
3. Klasyfikacja znaku liczby
Wczytaj liczbę całkowitą i sklasyfikuj ją jako dodatnią, ujemną albo równą zero. Zanim spojrzysz na rozwiązanie, odpowiedz: dlaczego nie wystarczą tylko przypadki „dodatnia” i „ujemna”?
Rozwiązanie#include <stdio.h>
int main(void) {
int number;
scanf("%d", &number);
if (number > 0) {
printf("dodatnia\n");
} else if (number < 0) {
printf("ujemna\n");
} else {
printf("zero\n");
}
return 0;
}
Trzy gałęzie są rozłączne i łącznie obejmują każdą liczbę całkowitą. Końcowe else oznacza tutaj dokładnie przypadek number == 0.
4. Czy b leży pomiędzy a i c?
Wczytaj trzy różne liczby. Sprawdź, czy b leży ściśle pomiędzy a i c, niezależnie od kolejności wartości skrajnych. Nie wolno zakładać, że a < c.
#include <stdio.h>
int main(void) {
double a, b, c;
scanf("%lf %lf %lf", &a, &b, &c);
if ((a < b && b < c) || (c < b && b < a)) {
printf("b lezy scisle pomiedzy a i c\n");
} else {
printf("b nie lezy scisle pomiedzy a i c\n");
}
return 0;
}
Pierwsza część warunku obsługuje kolejność rosnącą, a druga malejącą. Operator || oznacza, że wystarczy jeden z tych układów. Równość nie jest akceptowana, ponieważ użyliśmy porównań ostrych.
Łączenie warunków: &&, || i !
| Operator | Czytamy | Kiedy wynik jest prawdziwy? |
&& | i | gdy oba warunki są prawdziwe |
|| | lub | gdy przynajmniej jeden warunek jest prawdziwy |
! | nie | gdy negowany warunek jest fałszywy |
C stosuje obliczanie skrócone. Dla A && B drugi warunek nie jest sprawdzany, jeżeli A jest fałszywe. Dla A || B drugi warunek nie jest sprawdzany, jeżeli A jest prawdziwe. Dzięki temu zapis denominator != 0 && numerator / denominator > 2 nie wykona dzielenia przez zero.
5. Hierarchia podzielności
Wczytaj liczbę całkowitą. Wypisz najbardziej szczegółową z informacji: liczba jest podzielna przez 6; przez 3, ale nie przez 6; przez 2, ale nie przez 6; albo ani przez 2, ani przez 3. Zwróć uwagę, że przypadki matematycznie się nakładają – to kolejność gałęzi nadaje komunikatom jednoznaczność.
#include <stdio.h>
int main(void) {
int number;
scanf("%d", &number);
if (number % 6 == 0) {
printf("podzielna przez 6\n");
} else if (number % 3 == 0) {
printf("podzielna przez 3, ale nie przez 6\n");
} else if (number % 2 == 0) {
printf("podzielna przez 2, ale nie przez 6\n");
} else {
printf("nie jest podzielna przez 2 ani przez 3\n");
}
return 0;
}
Gdyby najpierw sprawdzić podzielność przez 2, liczba 12 nigdy nie dotarłaby do gałęzi podzielności przez 6. W łańcuchu else if często zaczynamy od przypadku najbardziej szczegółowego.
6. Czy z odcinków można zbudować trójkąt?
Wczytaj długości trzech odcinków. Sprawdź, czy są dodatnie i czy spełniają wszystkie nierówności trójkąta. Najpierw zapisz warunki słownie. Test graniczny powinien obejmować odcinki 1, 2, 3.
#include <stdio.h>
int main(void) {
double a, b, c;
scanf("%lf %lf %lf", &a, &b, &c);
int positive = a > 0.0 && b > 0.0 && c > 0.0;
int inequalities = a + b > c && a + c > b && b + c > a;
if (positive && inequalities) {
printf("mozna zbudowac trojkat\n");
} else {
printf("nie mozna zbudowac trojkata\n");
}
return 0;
}
Zmienne positive i inequalities przechowują wyniki dwóch sensownych pytań. Nazwanie części złożonego warunku często poprawia czytelność bardziej niż próba zapisania wszystkiego w jednym wierszu.
7. Rok przestępny
Rok jest przestępny, jeżeli dzieli się przez 400 albo dzieli się przez 4, ale nie przez 100. Zaimplementuj tę regułę i przygotuj cztery testy: 1900, 2000, 2023 i 2024.
#include <stdio.h>
int main(void) {
int year;
scanf("%d", &year);
int divisible_by_4 = year % 4 == 0;
int divisible_by_100 = year % 100 == 0;
int divisible_by_400 = year % 400 == 0;
if (divisible_by_400 || (divisible_by_4 && !divisible_by_100)) {
printf("rok przestepny\n");
} else {
printf("rok zwykly\n");
}
return 0;
}
1900 → zwykły2000 → przestępny2023 / 2024Test nie jest przypadkową liczbą
Dobry zestaw testów wynika ze struktury decyzji. Dla każdej gałęzi wybierz co najmniej jeden przykład. Dodaj wartości leżące dokładnie na granicy porównania oraz takie, które rozróżniają typowy błędny warunek od poprawnego.
- Wypisz przypadki wynikające ze specyfikacji.
- Znajdź granice: zero, równość, koniec przedziału.
- Przewidź wynik ręcznie.
- Sprawdź każdą gałąź programu.
Do omówienia na zajęciach: „Uruchomiłem i działa” oznacza tylko tyle, że program zadziałał dla jednego wejścia. Nie mówi, czy decyzja została opisana kompletnie.
Audyt kodu wygenerowanego przez AI
Poniższy program kompiluje się i dla wielu współczesnych lat podaje poprawny wynik. Mimo to nie implementuje pełnej reguły kalendarza gregoriańskiego.
Znajdź brakujący przypadek
#include <stdio.h>
int main(void) {
int year;
scanf("%d", &year);
if (year % 4 == 0 && year % 100 != 0) {
printf("rok przestepny\n");
} else {
printf("rok zwykly\n");
}
return 0;
}
Nie poprawiaj kodu od razu. Najpierw znajdź rok, dla którego wynik będzie błędny, i wyjaśnij, której części specyfikacji brakuje. Czy testy 2023 i 2024 wystarczyłyby do wykrycia problemu?
Wniosek z audytu: kod odrzuca wszystkie lata podzielne przez
100, również te podzielne przez400. Rok2000jest kontrprzykładem. Poprawna reguła znajduje się w zadaniu 7.
Podsumowanie
ifwykonuje blok tylko wtedy, gdy warunek jest prawdziwy,else ifbuduje uporządkowany wybór pierwszej pasującej gałęzi,- kolejność warunków może zmienić wynik programu,
- złożony warunek warto rozłożyć na nazwane, prostsze pytania,
- przypadek graniczny i kontrprzykład są ważniejsze niż wiele losowych testów.
Co trzeba rozumieć, nawet używając AI: jakie przypadki istnieją, czy się wykluczają, czy pokrywają całe wejście i jaki test ujawni pominiętą gałąź. Składnię można sprawdzić; odpowiedzialności za regułę decyzyjną nie można oddać generatorowi kodu.
Dalsza część pracy odbywa się na Platformie do ćwiczeń. Zadania utrwalające celowo wykorzystują inne problemy niż przykłady z tego wpisu.
Pytania kontrolne
- Jaka jest różnica między
=i==? - Co C uznaje za fałsz, a co za prawdę?
- Dlaczego w łańcuchu
else ifkolejność ma znaczenie? - Jak zapisać matematyczny warunek
a < b < c? - Kiedy drugi argument
&&nie zostanie obliczony? - Czym różni się test zwykły od granicznego i rozróżniającego?