Kurs podstawy programowania UMCS, zdjęcie laptopa.

Podstawy programowania – Laboratorium 3

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 if i else,
  • łą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.

OperatorZnaczeniePrzykład
==równex == 0
!=różnex != y
<, <=mniejsze, nie większeage >= 18
>, >=większe, nie mniejszetemperature > 0.0

= i == nie są zamienne. Zapis x = 5 zmienia stan programu, a x == 5 zadaje 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

Rdzeńpredict before runstan programu

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.

Program do analizy
#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

Rdzeńif–elsewarunek ochronny

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;
}
Zwykły10 4 → 2.500000
Granica10 0 → komunikat
Znak10 -4 → -2.500000

else 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

Rdzeńelse ifrozłączne przypadki

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?

Rdzeń&&||symetria

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.

Rozwiązanie
#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 !

OperatorCzytamyKiedy wynik jest prawdziwy?
&&igdy oba warunki są prawdziwe
||lubgdy przynajmniej jeden warunek jest prawdziwy
!niegdy 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

Rdzeńelse if%kolejność

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ść.

Rozwiązanie
#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?

Rdzeńdekompozycja warunkuprzypadki brzegowe

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.

Rozwiązanie
#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

Rdzeńpełna specyfikacjatesty rozróżniające

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.

Rozwiązanie
#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;
}
Wyjątek stulecia1900 → zwykły
Wyjątek od wyjątku2000 → przestępny
Zwykłe przypadki2023 / 2024

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

  1. Wypisz przypadki wynikające ze specyfikacji.
  2. Znajdź granice: zero, równość, koniec przedziału.
  3. Przewidź wynik ręcznie.
  4. 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

AI-auditkontrprzykładpełność warunku
#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 przez 400. Rok 2000 jest kontrprzykładem. Poprawna reguła znajduje się w zadaniu 7.

Podsumowanie

  • if wykonuje blok tylko wtedy, gdy warunek jest prawdziwy,
  • else if buduje 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

  1. Jaka jest różnica między = i ==?
  2. Co C uznaje za fałsz, a co za prawdę?
  3. Dlaczego w łańcuchu else if kolejność ma znaczenie?
  4. Jak zapisać matematyczny warunek a < b < c?
  5. Kiedy drugi argument && nie zostanie obliczony?
  6. Czym różni się test zwykły od granicznego i rozróżniającego?

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Wymagane pola są oznaczone *