Kurs podstawy programowania UMCS, zdjęcie laptopa.

Podstawy programowania – Laboratorium 1

Po co uczyć się programowania, skoro AI potrafi generować kod?

Coraz częściej programista nie zaczyna od pustego pliku. Może użyć dokumentacji, wyszukiwarki, gotowej biblioteki albo narzędzia AI. Nie zmienia to jednak podstawowego problemu: ktoś musi wiedzieć co program ma zrobić, rozpoznać błędne rozwiązanie, dobrać testy, zrozumieć komunikat kompilatora i zdecydować, czy wygenerowany kod rzeczywiście realizuje wymagania. Dlatego na tym kursie język C jest narzędziem do nauki sposobu myślenia o programie, a nie celem samym w sobie.

Kurs „Podstawy programowania” nie sprawi, że po jednym semestrze będziecie pamiętać z głowy każdy szczegół składni C. Natomiast ma przygotować do rozumienia programu, jakie dane przechowuje, w jakiej kolejności wykonuje operacje, co powinien zwrócić dla przykładowego wejścia i gdzie może wystąpić błąd.


Dlaczego właśnie taki jest cel kursu?

To podejście nie jest próbą ignorowania zmian wywołanych przez generatywne AI. Przeciwnie, podobny kierunek widać obecnie na najlepszych kursach wprowadzających do informatyki. MIT 6.100A nadal rozpoczyna od obiektów, typów, zmiennych, wejścia/wyjścia, sterowania przepływem i prostych algorytmów, a jednocześnie udostępnia własnego tutora AI do samodzielnej nauki. Stanford CS106A akcentuje projektowanie programu, dekompozycję, abstrakcję, testowanie i poprawny styl, a użycie generatywnego AI w ocenianych zadaniach jest ograniczone po to, aby student najpierw wykształcił własne umiejętności tworzenia i debugowania kodu. Harvard CS50 dopuszcza własne kontrolowane narzędzie AI, ale również podkreśla, że znajomość fundamentów pozwala korzystać z AI jako świadomy twórca. Oxford w pierwszym roku kładzie nacisk na rozumienie, dlaczego program działa poprawnie, oraz na systematyczne konstruowanie algorytmów. Zachęcam do zapoznania się z publicznymi materiałami wyżej wymienionych krusów.


Co powinieneś umieć po tych zajęciach?

Po laboratorium powinieneś potrafić:

  • wyjaśnić różnicę między problemem, algorytmem, kodem źródłowym i wykonywanym programem,
  • utworzyć, skompilować i uruchomić prosty program w C,
  • wskazać, od którego miejsca zaczyna się wykonanie programu,
  • użyć printf do wypisania wyniku i scanf do pobrania prostych danych,
  • zadeklarować zmienną typu całkowitego i zmiennoprzecinkowego,
  • wykonać podstawowe operacje arytmetyczne,
  • rozumieć różnicę między dzieleniem całkowitym i rzeczywistym,
  • przeczytać podstawowy komunikat kompilatora i poprawić prosty błąd,
  • przewidzieć wynik krótkiego programu bez jego uruchamiania,
  • ocenić, czy kod wygenerowany przez AI jest w ogóle kodem w wymaganym języku i czy realizuje specyfikację.

Nie musisz jeszcze: znać wskaźników, pamięci dynamicznej, funkcji użytkownika, tablic ani dokładnego modelu pamięci. Jeżeli jakaś konstrukcja wymaga tych pojęć, na razie będziemy używali jej w ograniczonym zakresie i wrócimy do niej później.


Najpierw problem, potem program

Programowanie nie zaczyna się od wpisania #include. Zaczyna się od odpowiedzi na pytanie: co właściwie chcemy obliczyć?

Załóżmy, że chcemy policzyć średnią trzech liczb. Możemy opisać rozwiązanie bez żadnego języka programowania:


  • Krok 1 – Pobierz trzy liczby a, b, c.
  • Krok 2 – Oblicz ich sumę.
  • Krok 3 – Podziel sumę przez 3.
  • Krok 4 – Wyświetl wynik.

Taki skończony opis sposobu rozwiązania problemu nazywamy algorytmem. Algorytm może być zapisany słownie, za pomocą diagramu, pseudokodu albo matematycznie. Dopiero potem zapisujemy go w konkretnym języku programowania.

Język programowania jest tylko sposobem zapisania rozwiązania. Jeżeli nie potrafimy powiedzieć, co program ma zrobić krok po kroku, narzędzie AI również będzie miało problem: albo zgadnie nasze intencje, albo wygeneruje poprawny składniowo program rozwiązujący niewłaściwe zadanie.

PojęcieZnaczenie
ProblemTo, co chcemy osiągnąć, np. „oblicz średnią trzech liczb”.
AlgorytmSposób rozwiązania problemu – kolejność operacji.
Kod źródłowyZapis algorytmu w konkretnym języku, np. C.
Program wykonywalnyPostać programu przygotowana do uruchomienia przez system.
ProcesUruchomiona instancja programu wykonująca instrukcje.

Pierwszy program w C

Napisz program w języku C, wypisujący „Hello World!”.

Rozwiązanie:
// Pierwszy program w C
#include <stdio.h>

int main(void) {
	printf("Hello world!\n");
	return 0;
}
Omówienie:

Na razie program jest krótki, ale występuje w nim kilka elementów, które będą pojawiały się przez cały kurs. Program w języku C, niezależnie od rozmiaru, jest zbudowany z funkcji i zmiennych/obiektów. Funkcja zawiera instrukcje określające, jakie operacje procesu obliczeniowego należy wykonać (zadania), zmienne zaś przechowują wartości używane podczas tego procesu (dane).


Linia 1: komentarze.
Czym są komentarze? Jak podają źródła w Internecie:

Komentarz jest to notatka sporządzona przez programistę, 
znajdująca się w kodzie źródłowym programu. 
Komentarz w żaden sposób nie wpływa na działanie programu.

W języku C mamy do dyspozycji trzy rodzaje komentarzy: jednowierszowy, wielowierszowy i wykonany za pomocą dyrektyw preprocesora.

Tych ostatnich nie będziemy omawiać w ramach kursu, jednakże mowa tutaj o #pragma message("Komentarz"). Takie komentarze pojawiają się podczas kompilacji programu, ale nie wpływają na jego logikę.

Komentarz jednowierszowy zaczyna się od dwóch ukośników // i trwa do końca wiersza. Uwaga: Komentarz nie działa wewnątrz tekstu w cudzysłowach:
printf("To // komentarzem nie będzie.");

Komentarz wielowierszowy rozpoczyna się znakami /* i kończy */. Na przykład:
/* jest to komentarz
w wielu liniach */


Generalnie zaleca się stosować komentarze jednowierszowe, gdy dodajemy jakieś opisy działania programu, co jest preferowaną praktyką ze względu na ich czytelność. Komentarz wielowierszowy przydaje się do dłuższych notatek lub tymczasowego wyłączenia fragmentu kodu. Ponadto często gdy chcemy wymienić jakiś stary algorytm na nowy, np. wersję źle działającego algorytmu komentujemy i w miejsce starego kodu piszemy nową, jednocześnie mając wgląd na to co wcześniej napisaliśmy. Jednakże należy zaznaczyć, że nie jest to idealna praktyka, a do rozwiązywania takich sytuacji programista ma inne narzędzia np. system kontroli wersji.

Wskazówka: Dobry kod komentuje się sam. Wraz ze zwiększaniem wiedzy i umiejętności programistycznych można mieć wrażenie, że komentarze są zbędne. Faktem jest, że programiści z większym doświadczeniem bardzo skromnie komentują kod lub nie robią tego wcale. Wynika to z dobrze napisanego kodu, poprawnie nazwanych zmiennych i funkcji. To nie znaczy, że komentarze nie są potrzebne są momenty, gdy są nieodzowne. Wręcz przeciwnie na początku nauki programowania mogą być bardzo przydatne.


Linia 2: dyrektywy.
Wszystkie wiersze, które zaczynają się znakiem # nazywamy dyrektywami preprocesora. Każda dyrektywa musi zaczynać się od nowego wiersza. Dyrektywy preprocesora to instrukcje, które są przetwarzane przed właściwą kompilacją programu.

Linia zaczynająca się od # jest przetwarzana przed właściwą kompilacją. Dyrektywa #include udostępnia deklaracje elementów z określonego pliku nagłówkowego – ich definicje znajdują się w innych plikach. stdio.h jest częścią standardowej biblioteki C i zawiera między innymi informacje potrzebne do użycia funkcji printf i scanf.

Na tym etapie wystarczy rozumieć: jeżeli korzystamy z funkcji standardowego wejścia/wyjścia, dołączamy stdio.h. Dokładniej omówimy podział programu na pliki i biblioteki pod koniec kursu.

Linia 4: miejsce rozpoczęcia programu.
Każdy zwykły program w C posiada funkcję main. To od niej rozpoczyna się wykonanie programu. Jest to główna funkcja programu. int oznacza, że funkcja kończąc działanie zwraca liczbę całkowitą. void w nawiasach okrągłych oznacza tutaj, że nie przekazujemy do funkcji żadnych argumentów. Nie znasz jeszcze funkcji i parametrów – i nie musisz. Na pierwszych zajęciach poniższy kod traktuj jako standardowy szkielet prostego programu.

int main(void) {
    /* instrukcje programu */
    return 0;
}

Linia 4-7: klamry { } – blok instrukcji.
Klamry grupują instrukcje należące do danego fragmentu programu. W tym przypadku wyznaczają ciało funkcji main. W kolejnych laboratoriach te same klamry będą grupowały także instrukcje warunkowe, pętle i funkcje zaimplementowane przez Ciebie.


Linia 5: wywołanie funkcji.
To wywołanie funkcji printf ze standardowej biblioteki wejścia-wyjścia. Funkcję wywołuje się podając jej nazwę i w nawiasach listę argumentów. Argumentem nazywamy wartości przekazywane do funkcji, kolejne argumenty są rozdzielone przecinkami.
Warto zauważyć i zapamiętać, że na końcu każdej instrukcji (lub grupy instrukcji) musi znaleźć się średnik ;, który informuje kompilator gdzie ta instrukcja się kończy.

Nasza funkcja posiada jeden argument i jest nim "Hello world!\n". Funkcja ta wypisuje tekst na standardowe wyjście (zazwyczaj ekran monitora i konsola), w tym przypadku ciąg znaków ujęty w znaki cudzysłów. "Hello world!\n" najczęściej nazywamy stałą napisową (ang. string), bądź po prostu napisem i na razie będziemy stosować je jedynie w argumentach funkcji printf. \n oznacza znak nowej linii. Nie są to dwa znaki wyświetlane na ekranie, jest to specjalna sekwencja oznaczająca przejście do kolejnego wiersza (ang. new line).

Należy dodać, że printf może przyjmować więcej niż jeden argument:
printf("%d", 3 + 4)
Tym razem funkcja printf przyjęła dwa argumenty rozdzielone przecinkiem: stałą napisową oraz działanie arytmetyczne. Porównując wywołanie funkcji z treścią którą otrzymamy (wyświetlona zostanie liczba 7) można łatwo zauważyć, że zamiast zapisu %d pokaże się nam liczba 7. Znak % w stałej napisowej jest znakiem specjalnym rozpoczynającym sekwencję sterującą, po którym następuje określenie sposobu prezentacji kolejnych argumentów funkcji printf. Zapis %d informuje, że argumentem będzie liczba całkowita. printf jest znacznie bardziej rozbudowana niż ten jeden podstawowy typ, ale inne poznamy wraz z trwaniem tego kursu.

Brak średnika jest jednym z najczęstszych błędów na początku nauki. Kompilator często wskaże jednak miejsce tuż po rzeczywistym błędzie, ponieważ dopiero wtedy zauważy, że poprzednia instrukcja nie została poprawnie zakończona.


Linia 6: zwracanie wartości przez funkcję.
Nasza funkcja main, poza wyświetleniem komunikatu, zawiera tylko jedno polecenie do wykonania: zakończ funkcję z wynikiem 0.
return 0;
Liczba ta będzie zwrócona do systemu operacyjnego jako wynik działania programu. Umownie zero oznacza poprawne zakończenie programu.

Kod źródłowy nie wykonuje się sam – kompilacja

Plik .c jest tekstem. Procesor nie wykonuje bezpośrednio takich instrukcji jak printf. Kod musi zostać przetworzony przez zestaw narzędzi, w tym kompilator i linker. Na potrzeby kursu możemy używać polecenia:
gcc -std=c17 -Wall -Wextra -Wpedantic main.c -o main
lub na początek prościej
gcc main.c
Najważniejsze elementy powyższych poleceń:

ElementZnaczenie
gccuruchamia kompilator/toolchain GCC
-std=c17prosimy o kompilowanie zgodnie ze standardem C17
-Wall -Wextra -Wpedanticwłączamy wiele przydatnych ostrzeżeń
main.cplik źródłowy
-o mainnazwa tworzonego programu wykonywalnego

W systemach Linux/macOS program uruchomimy zwykle:
./main

Ostrzeżenie kompilatora nie jest dekoracją. Program może się mimo niego skompilować, ale na tym kursie dążymy do kodu kompilującego się bez ostrzeżeń. Narzędzia AI również potrafią wygenerować kod z ostrzeżeniami lub kod zależny od innego języka/standardu, kompilator jest pierwszym niezależnym recenzentem.


Błędy są częścią programowania

Na początku bardzo łatwo utożsamić komunikat o błędzie z porażką. W praktyce jest odwrotnie: komunikat kompilatora jest informacją pomagającą zlokalizować problem. Omówmy błędy, które mogą pojawić się w języku C/C++.

  1. Błędy składniowe / kompilacji (syntax errors):
    Są to błędy związane z naruszeniem zasad składni języka, np. brak średnika, nieprawidłowe użycie nawiasów, czy literówki w nazwach funkcji. Kompilator nie jest w stanie skompilować programu, dopóki te błędy nie zostaną usunięte.
    Przykład:
    int a = 10 // Brak średnika
  2. Błędy linkowania (linker errors):
    Błędy te występują na ostatnim etapie budowania programu, kiedy kompilator próbuje połączyć pliki obiektowe (.o lub .obj) oraz biblioteki w jeden plik wykonywalny. Najczęstszym komunikatem, który sygnalizuje ten problem, jest undefined reference to....
    Przykład:
    Brak kompilacji wszystkich plików źródłowych, literówki w nazwach funkcji lub zmiennych np. calSum() w pliku nagłówkowym, zaś w kodzie źródłowym calsum().
  3. Błędy wykonania (runtime errors):
    Błędy te pojawiają się w trakcie działania programu i są związane z nieprzewidzianymi sytuacjami, które mogą spowodować zakończenie programu z błędem. Mogą być wynikiem operacji takich jak dzielenie przez zero, dostęp do nieprawidłowego obszaru pamięci (segmentation fault) lub wyczerpanie zasobów systemowych (np. pamięci). Takie błędy czasami nazywamy także błędami semantycznymi krytycznymi.
    Przykład:
    int a = 10, b = 0;
    int wynik = a / b; // Dzielenie przez zero

  4. Błędy logiczne (logical errors):
    Błędy logiczne to błędy w algorytmie lub logice programu. Program kompiluje się poprawnie i uruchamia się, ale nie działa zgodnie z oczekiwaniami i daje błędne wyniki. Kompilator nie wykryje takich błędów, ponieważ z punktu widzenia składni wszystko jest poprawne, ale logika programu jest niewłaściwa. Takie błędy nazywamy także błędami semantycznymi.
    Przykład:
    int a = 10, b = 5;
    int wynik = a - b; // Powinno być a + b, ale kod daje zły wynik (5 zamiast 15)

Ostatnia kategoria będzie dla nas szczególnie ważna w świecie generowania kodu. AI często potrafi wygenerować program, który wygląda przekonująco i kompiluje się poprawnie, ale realizuje nieco inną interpretację zadania.

Napraw program

Poniższy program zawiera kilka prostych błędów. Popraw je. Nie przepisuj programu od nowa. Skompiluj go. Przeczytaj pierwszy komunikat kompilatora. Popraw jeden problem i skompiluj ponownie. Powtarzaj aż program będzie poprawny. Dodatkowo, zapisz dla każdego problemu:

  • co zgłosił kompilator,
  • jaka była rzeczywista przyczyna,
  • co zostało zmienione.

#include <stdio.h>

int main(void) {
    pritnf("Programowanie\n")
    printf("w jezyku C\n");

    return 0;
}

Przewidywanie wykonania programu

Programista powinien umieć mentalnie wykonać krótki fragment kodu. Jest to jedna z najważniejszych umiejętności przy debugowaniu i weryfikacji kodu wygenerowanego przez AI.

Najpierw przewidź, potem uruchom

Bez uruchamiania programu zapisz dokładnie, co pojawi się na ekranie. Dopiero później skompiluj program i porównaj wynik.

#include <stdio.h>

int main(void) {
    printf("A");
    printf("B\n");
    printf("C\tD\n");
    printf("\n\n");
    return 0;
}

Zmienne – dane, które program pamięta

Programy są użyteczne dlatego, że operują na danych. Do przechowywania wartości używamy między innymi zmiennych.
int age = 20;
float width = 50.f;
double temperature = 21.5;

Zmienna jest to pewien fragment pamięci o ustalonym rozmiarze, który posiada własny identyfikator (nazwę) oraz może przechowywać pewną wartość, zależną od typu zmiennej. W uproszczeniu zmienna ma: nazwę (np. age), typ (np. int), wartość (np. 20). Typ, na przykład int służy do przechowywania liczb całkowitych (np. -3, 0, 42), zaś float lub double jest typem zmiennoprzecinkowym używanym do reprezentacji liczb z częścią ułamkową, kolejno pojedynczej lub podwójnej precyzji (np. 3.14, 123.12f, 42.0).

Nazwy zmiennych nie mogą zawierać polskich znaków diakrytycznych, a dozwolone znaki to: małe litery ( a...z ), duże litery ( A...Z ), podkreślenie ( _ ) i cyfry ( 0...9 ). Ponadto nazwa zmiennej nie może zaczynać się od liczby, zmienne muszą posiadać unikatowe nazwy w obrębie całego bloku. Należy nadmienić, że zmienna int a; nie jest tą samą zmienną co int A;, są to dwie różne zmienne, o różnych nazwach.

Zasięg zmiennych. Zmienne, które zdefiniowaliśmy wewnątrz funkcji main mają zasięg lokalny i nazywamy je zazwyczaj zmiennymi lokalnymi. Zmienna lokalna widoczna jest tylko w obrębie bloku, w którym została zdefiniowana, czyli w obrębie klamry { ... }. Istnieją także inne rodzaje zmiennych np. globalne i statyczne, te pierwszy omówimy w przyszłości.

Na razie nie zapamiętuj pełnej listy typów ani ich wszystkich zakresów. Ważniejsze jest pytanie: czy mój problem wymaga liczby całkowitej, czy może wyniku z częścią ułamkową? Szczegółowo typami zajmiemy się na kolejnych laboratoriach.


Deklaracja, definicja, inicjalizacja i przypisanie

Deklaracja jest informacją dla kompilatora, że dana nazwa jest już znana. Jednakże w tym momencie pamięć dla obiektu nie jest jeszcze przydzielona. Do obiektu nie można się odwołać, nie można mu przypisać wartości, bowiem on tak na prawdę jeszcze nie istnieje. Deklaracja występuje w kontekście zmiennych, funkcji, czy typów danych. Należy dodać, że w programie może być kilka deklaracji tego samego elementu.

Deklaracje zmiennych znajdują się zazwyczaj w plikach nagłówkowych, zaś definicje w plikach źródłowych. Niestety w trakcie trwania tego kursu rzadko będziemy spotykać deklaracje zmiennych. Z drugiej strony należy pamiętać, że deklaracje mają duże znaczenie. W trakcie omawiania funkcji, częściej skorzystamy z deklaracji w naszych programach. Należy dodać, że w przypadku funkcji deklaracja nazywana jest również prototypem funkcji.

Definicja w przeciwieństwie do deklaracji nie informuje tylko, że dany identyfikator istnieje w programie, ale także dokładnie określa, czym on jest. Definicja rezerwuje miejsce w pamięci dla zadeklarowanej zmiennej i już można jej przypisać wartość (w przypadku funkcji definicja zawiera ciało funkcji, czyli jej implementację). Podsumowując każda definicja jest jednocześnie deklaracją, ale ta zależność nie zachodzi w drugą stronę.

Z definicją mieliśmy już styczność wyżej podczas tworzenia zmiennej w pliku z kodem źródłowym (np. definicje zmiennych typ nazwa_zmiennej;). Podobnie, jak w przypadku deklaracji, z definicji będziemy korzystać również przy omawianiu funkcji.

Inicjalizacja (inicjowanie) polega na przypisaniu wartości do danej zmiennej w momencie jej definicji. Aby zainicjować zmienną piszemy:
typ nazwa_zmiennej = wartosc;
Inicjalizacja następuje jedynie w przypadku przypisania wartości w momencie deklaracji. Przypisanie wartości danej zmiennej w dalszej części programu nie jest już inicjalizacją, tylko zwykłym przypisaniem. Tym samym inicjalizacja służy do przypisania początkowej wartości zmiennej i nie jest ona obowiązkowa. Jednakże warto pamiętać, że niezainicjalizowane zmienne automatyczne będą zawierać niezdefiniowane dane, które były w pamięci.

Przykładem inicjalizacji jest:
int a = 1;
, ale nie jest nim:
int a;
a = 1;


Operator = w C nie oznacza „jest równe” w sensie matematycznym. Jest to operator przypisania: wartość po prawej stronie zostaje obliczona i zapisana do zmiennej po lewej. Dlatego taki zapis jest poprawny:
int x = 4;
x = x + 1;
Po drugiej instrukcji x ma wartość 5. Matematyczne równanie x = x + 1 nie miałoby rozwiązania, ale instrukcja programu opisuje zmianę stanu.

Ważne! To pierwszy moment, w którym warto bardzo mocno odróżnić matematykę od programu. Program wykonuje operacje w czasie. Zmienna może mieć inną wartość przed instrukcją i inną po niej.

Poniżej przedstawiono podstawowe operacje arytmetyczne. Należy pamiętać, że obowiązują reguły priorytetu zbliżone do znanych z matematyki. Jeżeli zależy nam na jednoznaczności i czytelności, używamy nawiasów okrągłych.

OperatorZnaczeniePrzykład
+dodawaniea + b
–odejmowaniea – b
*mnożeniea * b
/dzieleniea / b
%reszta z dzielenia całkowitegoa % b

Pierwsze obliczenia

Napisz program tworzący dwie zmienne całkowite a i b o dowolnych wartościach. Następnie program powinien wyświetlić ich sumę, różnicę oraz iloczyn.

Rozwiązanie:
#include <stdio.h>

int main(void) {
    int a = 7;
    int b = 3;

    printf("suma = %d\n", a + b);
    printf("roznica = %d\n", a - b);
    printf("iloczyn = %d\n", a * b);

    return 0;
}
Omówienie:

Do tej pory printf wypisywał głównie tekst. Nazwa funkcji oznacza print formatted – wypisz dane według formatu.
int value = 42;
printf("Wartosc: %d\n", value);
%d jest znacznikiem miejsca, w którym ma zostać wypisana wartość typu int. Na początku kursu wystarczy znać kilka formatów:

FormatTyp / zastosowanie
%dint
%uunsigned int – pojawi się później
%ffloat lub double
%.2fwypisanie dwóch cyfr po kropce
%cchar – pojedynczy znak

W C format nie jest tylko kosmetyką. printf musi otrzymać dane zgodne z opisem w napisie formatującym. Narzędzia AI czasem mylą specyfikatory, kompilowanie z ostrzeżeniami pomaga wychwytywać część takich błędów.


Pobieranie danych

Program staje się bardziej interesujący, gdy działa dla danych podanych podczas uruchomienia:

#include <stdio.h>

int main(void) {
    int a, b;

    scanf("%d %d", &a, &b);
    printf("suma = %d\n", a + b);

    return 0;
}
Omówienie:

Co oznacza scanf("%d %d", &a, &b);? scanf odczytuje dane ze standardowego wejścia. Napis "%d %d" mówi, że oczekujemy dwóch wartości całkowitych oddzielonych białym znakiem. Przy zmiennych występuje znak &: &a. Jest to operator pobierania adresu. Funkcja scanf potrzebuje informacji, gdzie w pamięci ma zapisać odczytaną wartość.

To jest pierwszy kontakt ze wskaźnikowym modelem pamięci C. Nie musisz jeszcze rozumieć wskaźników. Na tym etapie zapamiętaj sens: scanf ma zmienić wartość zmiennej, dlatego przekazujemy miejsce, do którego ma ją zapisać. Wrócimy do tego mechanizmu szczegółowo przy wskaźnikach.

Dla scanf wartość typu double wczytujemy formatem %lf:
double x;
scanf("%lf", &x);
To jeden z tych szczegółów, których w przyszłości nie trzeba heroicznie trzymać w pamięci, można zajrzeć do dokumentacji. Trzeba jednak rozumieć, że funkcja oczekuje informacji o typie danych i błędny format może prowadzić do niepoprawnego działania.

Pole trapezu

Napisz program, który wczyta od użytkownika trzy liczby zmiennoprzecinkowe: długość podstaw a, b oraz wysokość h. Oblicz pole trapezu:
P = (a + b) · h / 2
Następnie wypisz wynik na standardowe wyjście z dokładnością do dwóch miejsc po przecinku.

include <stdio.h>

int main(void) {
    double a, b, h;
    scanf("%lf %lf %lf", &a, &b, &h);
    
    double area = (a + b) * h / 2.0;
    printf("%.2lf\n", area);
    /*
    lub bez tworzenia dodatkowej zmiennej area
    printf("%.2lf\n", (a + b) * h / 2.0);
    */

    return 0;
}

Dzielenie całkowite – pierwszy ważny przypadek typu danych

Spójrz na dwa wyrażenia:
5 / 2
5.0 / 2.0
Pierwsze operuje na liczbach całkowitych i daje wynik całkowity 2. Drugie wykonuje dzielenie zmiennoprzecinkowe i daje 2.5. To nie jest błąd C, jest to konsekwencja typów argumentów operacji.

Rzutowanie to konwersja danej jednego typu na daną innego typu. Konwersja może być wykonana niejawnie (automatyczna, domyślna konwersja przyjęta przez kompilator) lub jawnie (wymuszona, zaimplementowana przez programistę za pomocą operatora rzutowania np. (float)v).

Przykłady konwersji niejawnej:
int i = 42.7; /* konwersja z double do int */
float f = i; /* konwersja z int do float */
double d = f; /* konwersja z float do double */
unsigned u = i; /* konwersja z int do unsigned int */
f = 4.2; /* konwersja z double do float */
i = d; /* konwersja z double do int */


Kompilator automatycznie dokonuje konwersji niejawnej, jeśli uzna, że jest to możliwe i bezpieczne. Podczas konwersji zmiennych zawierających większe ilości danych do typów prostszych (np. double do int) musimy liczyć się z utratą informacji. Gdy dokonujemy konwersji liczby zmiennoprzecinkowej do liczby całkowitej nie możemy przechowywać części ułamkowej toteż zostaje ona odcięta.

Konwersja jawna. Możemy powiedzieć kompilatorowi, że chcemy potraktować wartość jako konkretny typ np. double:
(int)3.9
(double)5
W praktyce przy średniej można napisać:
double average = (double)(a + b + c) / 3;
albo prościej:
double average = (a + b + c) / 3.0;

Przykłady konwersji jawnej:
double d = 3.14;
int pi_1 = (int)d;
float pi_2 = (float)d;
float pi_3 = pi_1 * 1.f;

Wskazówka. Należy unikać sytuacji, gdy wymuszamy konwersję i nigdy nie stosować rzutowania w celu „uciszenia kompilatora”. Rzutowanie nie jest metodą na naprawienie błędów, ale narzędziem, które należy używać ostrożnie. Zanim użyjemy operatora rzutowania należy się zastanowić, co tak na prawdę będzie on robił, jakie przyniesie efekty, czy też nie ma innego sposobu wykonania danej operacji, który wyeliminowałby podejmowanie takich kroków. Lepszą praktyką zamiast wymuszania rzutowania jest poprawne zarządzanie typami zmiennych.

Znajdź problem

Program ma policzyć średnią trzech liczb całkowitych. Przewidź wynik przed uruchomieniem. Następnie popraw program.

#include <stdio.h>

int main(void) {
    int a = 1, b = 2, c = 2;

    double average = (a + b + c) / 3;
    printf("%f\n", average);

    return 0;
}

Przypadki testowe

Często w takich sytuacjach pomocne są przypadki testowe, które umożliwiają zweryfikowanie poprawność działania programu dla różnych danych wejściowych. Innymi słowy nie wystarczy uruchomić programu raz. Zanim uznasz rozwiązanie za poprawne, pomyśl o danych, które sprawdzają różne własności programu. Dla programu liczącego średnią można użyć na przykład:

  • 3 6 9 – prosty przypadek, średnia całkowita,

  • 1 2 2 – wymusza wynik ułamkowy,

  • -2 0 2 – sprawdza liczby ujemne i zero,

  • 0 0 0 – przypadek graniczny.

Dobry test to nie „losowe liczby”. Dobry test ma powód istnienia. W przyszłości, gdy AI wygeneruje większy program, właśnie dobór przypadków testowych będzie jednym z głównych sposobów sprawdzenia, czy rozwiązanie rzeczywiście jest poprawne.


AI jako narzędzie do weryfikacji, nie proteza myślenia

W ramach tego kursu (i nie tylko) zachęcam do świadomego korzystania z narzędzi AI. Nie chodzi o udawanie, że takich narzędzi nie ma. Chodzi o nauczenie się pracy, w której człowiek zachowuję kontrolę nad problemem i wynikiem. Dobry schemat pracy wygląda następujaco:

  • przeczytaj problem i określ wejście oraz oczekiwany wynik,
  • spróbuj zapisać algorytm albo przynajmniej kilka kroków rozwiązania,
  • jeżeli już korzystasz z AI, traktuj wygenerowany kod jako propozycję – bynajmniej na tym etapie, gdy dopiero uczysz się programowania,
  • skompiluj kod z ostrzeżeniami,
  • przygotuj testy,
  • zmień jedno wymaganie i sprawdź, czy potrafisz sam zmodyfikować program.

Jeżeli jedyną metodą naprawy kodu jest ponowne wklejenie całego programu do AI i poproszenie „napraw to”, to tracimy informację, czy sami rozumiemy rozwiązanie. Przy prostych ćwiczeniach będziemy więc często najpierw pracować bez AI, a dopiero później używać go do recenzji albo porównania rozwiązań.

AI wygenerowało „program w C”

Otrzymaliśmy kod wygenerowany przez narzędzie AI. AI twierdzi, że jest to program w C. Odpowiedz na następujące pytania:

  • Czy to prawda?
  • Jakie elementy kodu zdradzają rzeczywisty język?
  • Przepisz program na czysty C.
  • Skompiluj go jako C.

#include <iostream>
using namespace std;

int main() {
    cout << "Witaj na kursie C!" << endl;
    return 0;
}
// Powyższy kod został napisany w C++, nie C. Charakterystyczne są m.in. <iostream>, using namespace std, cout i endl.
#include <stdio.h>

int main(void) {
    printf("Witaj na kursie C!\n");
    return 0;
}

Podsumowanie

Pozostałe zadania znajdziesz na Platformie do ćwiczeń. Zachęcam do zajrzenia na platformę i rozwiązanie zadań, pomogą utrwalić zdobytą wiedzę.

Co naprawdę warto zapamiętać?

Po dzisiejszych laboratoriach warto rozumieć i utrwalić następujące elementy:

  • co jest wejściem i wynikiem programu.
  • kolejność wykonywania instrukcji,
  • znaczenie zmiennej i jej typu,
  • różnicę między przypisaniem a równością matematyczną,
  • różnicę między dzieleniem całkowitym i rzeczywistym,
  • po co istnieją testy,
  • co oznacza błąd kompilacji i błąd logiczny.
Pamiętaj, że pozostałe elementy możesz zawsze sprawdzić w dokumentacji lub za pomocą narzędzi AI, będą to:
  • dokładny specyfikator formatu dla rzadko używanego typu,
  • pełną listę flag kompilatora,
  • nazwę mało używanej funkcji bibliotecznej,
  • szczegóły standardu, których nie używasz na co dzień.

Kluczowa zasada kursu: składnię można sprawdzić. Jeżeli jednak nie rozumiesz modelu obliczenia, nie będziesz wiedzieć, czego szukać, jak sformułować polecenie dla AI ani czy otrzymane rozwiązanie jest prawidłowe.

Pytania kontrolne

Spróbuj odpowiedzieć bez uruchamiania kompilatora:
1. Jaka jest różnica między algorytmem a programem?
2. Od jakiej funkcji zaczyna się wykonanie zwykłego programu w C?
3. Do czego służy #include <stdio.h>?
4. Co oznacza \n?
5. Dlaczego 5 / 2 i 5.0 / 2.0 mogą dać różne wyniki?
6. Dlaczego przy scanf("%d", &x) występuje &?
7. Czym różni się błąd kompilacji od błędu logicznego?
8. Podaj przykład testu, który ujawni błąd dzielenia całkowitego przy obliczaniu średniej.
9. Co powinieneś zrobić z kodem wygenerowanym przez AI przed uznaniem go za rozwiązanie?


Dodaj komentarz

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