Kurs podstawy programowania UMCS, zdjęcie laptopa.

Podstawy programowania – Laboratorium 2

Dane mają typ i ten typ zmienia wynik obliczeń

Komputer nie przechowuje po prostu „liczb”. Każda wartość ma określony sposób reprezentacji, rozmiar i zestaw operacji, które można na niej wykonać. Ten sam zapis działania może dać inny wynik zależnie od typów jego argumentów.

Podczas tego laboratorium skupimy się na świadomym dobieraniu danych do problemu. Nie chodzi o zapamiętanie katalogu typów C. Chodzi o umiejętność odpowiedzi na trzy pytania: co przechowuję, jaki zakres wartości jest potrzebny i jakiego wyniku oczekuję?

Co powinieneś umieć po tych zajęciach?

Po laboratorium powinieneś rozumieć nie tylko składnię, lecz przede wszystkim konsekwencje wyboru typu danych.

  • dobrać int, typ bez znaku albo double do prostego problemu,
  • odróżnić wartość, typ, zapis i sposób wyświetlenia danych,
  • sprawdzić rozmiar typu za pomocą sizeof,
  • odczytać podstawowe zakresy z limits.h,
  • poprawnie dobrać formaty printf i scanf,
  • wyjaśnić wynik dzielenia całkowitego i operatora %,
  • rozpoznać konwersję niejawną i wykonać jawną konwersję typu,
  • użyć wybranych funkcji z math.h,
  • przełożyć wzór matematyczny na kolejność obliczeń w C,
  • zaprojektować test ujawniający błąd związany z typem danych.

Nie ucz się tabeli typów na pamięć. Zapamiętaj podstawowe kategorie i pytania, które trzeba zadać. Dokładny zakres rzadziej używanego typu możesz później sprawdzić w dokumentacji albo bezpośrednio w programie.


Wartość, typ, reprezentacja i format

Te cztery pojęcia są ze sobą związane, ale nie oznaczają tego samego. Wartość jest informacją, na której pracujemy, np. liczba 42. Typ określa, jakiego rodzaju jest to informacja i jakie operacje są dla niej przewidziane. Reprezentacja opisuje sposób zapisania wartości w pamięci. Format określa natomiast sposób jej wczytania albo pokazania człowiekowi.

PojęciePytanie kontrolnePrzykład
WartośćCo chcemy przechować?42
TypJak program ma interpretować dane?int
ReprezentacjaJak wartość jest zakodowana w pamięci?ciąg bitów
FormatJak wczytujemy lub wyświetlamy wartość?%d, %X

Ta sama wartość może mieć różne zapisy. Liczba czterdzieści dwa może zostać pokazana jako 42, 052 albo 0x2A. Zmiana sposobu wyświetlania nie musi oznaczać zmiany wartości przechowywanej przez program.

1. Dobór typu do problemu

Rdzeńmodel danychbez kodowania

Dla każdej informacji wybierz spośród typów int, unsigned int, long long i double najprostszy rozsądny typ. Uzasadnij wybór jednym zdaniem. Jeżeli brakuje Ci informacji do podjęcia decyzji, powiedz jakiej.

  • liczba studentów w grupie,
  • temperatura ciała w stopniach Celsjusza,
  • liczba bajtów dużego pliku,
  • czas obliczeń podany w sekundach z dokładnością do milisekund,
  • saldo konta, które może być dodatnie lub ujemne.
Omówienie

Nie dla każdego przypadku istnieje jedna bezwzględnie poprawna odpowiedź. Liczy się jawne określenie zakresu, znaku i wymaganej dokładności. Przykładowo saldo może sugerować double, ale w rzeczywistym systemie finansowym częściej przechowywalibyśmy najmniejszą jednostkę waluty jako liczbę całkowitą.


Podstawowe typy danych

Na tym etapie wystarczy kilka typów. int służy do liczb całkowitych, unsigned int do nieujemnych liczb całkowitych, char przechowuje najmniejszą jednostkę adresowalnej pamięci i często służy do znaków, a float oraz double reprezentują liczby zmiennoprzecinkowe. W obliczeniach wykonywanych podczas kursu najczęściej będziemy używać double, gdy potrzebna jest część ułamkowa.

TypTypowe zastosowaniePrzykładowa wartość
intliczniki, indeksy, małe liczby całkowite-12, 0, 2026
unsigned intwartości nieujemne i operacje bitowe42u
long longwiększe liczby całkowite9000000000LL
charpojedyncze znaki i małe jednostki danych'A'
floatliczby zmiennoprzecinkowe o mniejszej precyzji3.5f
doubledomyślny wybór do obliczeń z częścią ułamkową3.5

Rozmiar typu: operator sizeof

Operator sizeof zwraca rozmiar obiektu albo typu wyrażony w bajtach C, czyli jednostek o rozmiarze char. Wynik ma typ size_t, dlatego w printf wypisujemy go za pomocą %zu. Standard gwarantuje, że sizeof(char) wynosi 1, ale taki bajt nie na każdej platformie musi mieć dokładnie osiem bitów. Liczbę bitów w bajcie określa makro CHAR_BIT. Wartość CHAR_BIT znajdziemy w limits.h.

Nie zakładaj bez sprawdzenia, że każdy typ ma zawsze ten sam rozmiar. C definiuje wymagane relacje i minimalne zakresy, ale część szczegółów zależy od implementacji oraz platformy.

2. Ile miejsca zajmują typy?

Rdzeńsizeoflimits.h

Wypisz rozmiary podstawowych typów oraz wybrane granice typów całkowitych. Uruchom program na komputerze w laboratorium i porównaj wynik z wynikiem innej osoby. Następnie odpowiedz: które wartości wynikają ze standardu, a które z konkretnej platformy?

Rozwiązanie
#include <limits.h>
#include <stdio.h>

int main(void) {
    printf("char:      %zu bajt(ow)\n", sizeof(char));
    printf("int:       %zu bajt(ow)\n", sizeof(int));
    printf("long:      %zu bajt(ow)\n", sizeof(long));
    printf("long long: %zu bajt(ow)\n", sizeof(long long));
    printf("float:     %zu bajt(ow)\n", sizeof(float));
    printf("double:    %zu bajt(ow)\n", sizeof(double));

    printf("INT_MIN = %d\n", INT_MIN);
    printf("INT_MAX = %d\n", INT_MAX);
    printf("UINT_MAX = %u\n", UINT_MAX);
    return 0;
}

Formaty wejścia i wyjścia

Funkcje printf i scanf korzystają z napisu formatującego. Kompilator widzi argumenty różnych typów, natomiast funkcja musi otrzymać informację, jak ma je interpretować. Niezgodność formatu z typem nie jest jedynie problemem estetycznym – może prowadzić do ostrzeżeń, błędnych wyników albo niezdefiniowanego zachowania.

Typprintfscanf
int%d%d
unsigned int%u, %o, %x%u, %o, %x
long long%lld%lld
unsigned long long%llu%llu
double%f%lf
size_t%zu%zu

Zwróć uwagę na double. W printf używamy %f, natomiast w scanf – %lf. Te funkcje mają inne reguły przekazywania argumentów. Nie próbuj uogólniać formatu jednej funkcji na drugą.

3. Audyt formatów printf

Rdzeńtyp a formatostrzeżenia

Poniższy program używa niewłaściwych specyfikatorów formatu. Przed kompilacją wskaż każdą niezgodność. Następnie skompiluj program z -Wall -Wextra -Wpedantic, przeczytaj ostrzeżenia i popraw kod.

#include <stdio.h>

int main(void) {
    int students = 12;
    double temperature = 21.5;

    printf("studenci: %f\n", students);
    printf("temperatura: %d\n", temperature);
    return 0;
}
Poprawiona wersja
#include <stdio.h>

int main(void) {
    int students = 12;
    double temperature = 21.5;

    printf("studenci: %d\n", students);
    printf("temperatura: %.1f\n", temperature);
    return 0;
}

4. Ta sama liczba w trzech systemach

Rdzeńprintfformat danych

Wczytaj dodatnią liczbę całkowitą w zapisie dziesiętnym, a następnie wypisz tę samą wartość dziesiętnie, ósemkowo i szesnastkowo. Sprawdź program dla 10, 16, 42 oraz 255.

Rozwiązanie
#include <stdio.h>

int main(void) {
    unsigned int number;
    scanf("%u", &number);

    printf("dziesietnie:     %u\n", number);
    printf("osemkowo:        %o\n", number);
    printf("szesnastkowo:    %X\n", number);
    return 0;
}

Program nie wykonuje tu konwersji wartości na trzy różne liczby. Zmienia jedynie sposób prezentacji tej samej wartości.


Konwersje typów

Konwersja niejawna zachodzi automatycznie, gdy język przekształca wartość do typu potrzebnego w danym wyrażeniu albo przypisaniu. Konwersję jawną zapisujemy operatorem rzutowania, np. (double)x albo (int)value. Rzutowanie nie „ulepsza” danych – zmienia sposób, w jaki mają być potraktowane, i może powodować utratę informacji.

Typ zmiennej wynikowej nie cofa wcześniejszego obliczenia. W zapisie double result = 5 / 2; najpierw wykonywane jest całkowite 5 / 2, a dopiero wynik 2 zostaje zapisany jako 2.0.


Funkcje matematyczne z math.h

Standardowa biblioteka matematyczna udostępnia między innymi sqrt, pow, fabs, sin i cos. Aby użyć ich deklaracji, dołączamy #include <math.h>. Na części systemów uniksowych podczas linkowania trzeba dodatkowo podać flagę -lm.

gcc -std=c17 -Wall -Wextra -Wpedantic program.c -lm -o program

Nie używaj pow(x, 2) automatycznie do każdego kwadratu. Prosty zapis x * x jest czytelny, nie wymaga ogólnej funkcji potęgowej i dokładniej pokazuje wykonywaną operację.

5. Odległość dwóch punktów

Utrwalającezmienne pomocniczemath.h

Wczytaj współrzędne punktów (x1, y1) i (x2, y2), a następnie oblicz ich odległość euklidesową. Użyj zmiennych pomocniczych dx oraz dy, aby kod odzwierciedlał kolejne kroki algorytmu.

Rozwiązanie
#include <math.h>
#include <stdio.h>

int main(void) {
    double x1, y1, x2, y2;
    scanf("%lf %lf %lf %lf", &x1, &y1, &x2, &y2);

    double dx = x2 - x1;
    double dy = y2 - y1;
    double distance = sqrt(dx * dx + dy * dy);

    printf("%.6f\n", distance);
    return 0;
}

Dzielenie całkowite i operator reszty

Jeżeli oba argumenty operatora / są całkowite, C wykonuje dzielenie całkowite. Część ułamkowa zostaje odrzucona, a od standardu C99 wynik jest obcinany w kierunku zera. Dlatego 5 / 2 daje 2, a -5 / 2 daje -2. Gdy przynajmniej jeden argument jest zmiennoprzecinkowy, otrzymujemy dzielenie zmiennoprzecinkowe.

WyrażenieTyp operacjiWynik
5 / 2dzielenie całkowite2
5.0 / 2dzielenie zmiennoprzecinkowe2.5
(double)5 / 2dzielenie zmiennoprzecinkowe2.5
5 % 2reszta z dzielenia1

Operator % działa na typach całkowitych i zwraca resztę z dzielenia. Dla niezerowego b zachodzi zależność:

a == (a / b) * b + a % b

Para operatorów / i % jest szczególnie użyteczna, gdy rozbijamy jedną liczbę na większe i mniejsze jednostki albo kodujemy kilka składowych w jednej wartości.

6. Sekundy na format hh:mm:ss

Rdzeń/%dekompozycja

Wczytaj nieujemną liczbę sekund i rozbij ją na godziny, minuty oraz sekundy. Przed napisaniem programu ręcznie wykonaj algorytm dla 3725. Potem przetestuj także 0, 59, 60, 3599 i 3600.

Rozwiązanie
#include <stdio.h>

int main(void) {
    unsigned int total_seconds;
    scanf("%u", &total_seconds);

    unsigned int hours = total_seconds / 3600;
    unsigned int remainder = total_seconds % 3600;
    unsigned int minutes = remainder / 60;
    unsigned int seconds = remainder % 60;

    printf("%02u:%02u:%02u\n", hours, minutes, seconds);
    return 0;
}
Przypadek zwykły3725 → 01:02:05
Granica jednostki60 → 00:01:00
Wartość zerowa0 → 00:00:00

7. Czas transmisji pliku

Rdzeńjednostkimodel → kod

Podczas transmisji do każdych ośmiu bitów danych dołączane są dwa bity kontrolne. Wczytaj rozmiar pliku w megabajtach oraz przepustowość łącza w megabitach na sekundę. Oblicz czas przesyłania przy wykorzystaniu pełnej przepustowości.

Aby zadanie było jednoznaczne, przyjmij: 1 MB = 1024 · 1024 B, 1 B = 8 bitów oraz 1 Mbit/s = 1 000 000 bit/s. Dwa bity kontrolne zwiększają koszt przesłania każdego bajtu z 8 do 10 bitów.

Zanim napiszesz program, rozpisz łańcuch jednostek: MB → B → bity po dodaniu narzutu → sekundy. Test kontrolny: plik 1 MB i łącze 10 Mbit/s.

Rozwiązanie
#include <stdio.h>

int main(void) {
    double file_size_mb, bandwidth_mbps;
    scanf("%lf %lf", &file_size_mb, &bandwidth_mbps);

    double file_size_bytes = file_size_mb * 1024.0 * 1024.0;
    double transmitted_bits = file_size_bytes * 10.0;
    double bandwidth_bps = bandwidth_mbps * 1000000.0;
    double transmission_time = transmitted_bits / bandwidth_bps;

    printf("czas transmisji: %.6f s\n", transmission_time);
    return 0;
}

Dla danych kontrolnych wynik wynosi około 1.048576 s. Gdybyśmy pominęli narzut, otrzymalibyśmy około 0.838861 s. Różnica ma więc jasne znaczenie fizyczne, a nie wynika jedynie ze składni programu.


Od wzoru matematycznego do programu

Wzór jest zwartym opisem zależności. Program musi dodatkowo określić dane wejściowe, typ każdej wartości, kolejność działań, warunki poprawności oraz sposób prezentacji wyniku. Dlatego przed kodowaniem zastosuj cztery kroki:

  1. Nazwij dane i ich jednostki.
  2. Zapisz wzór albo pseudokod.
  3. Dobierz typy i kolejność operacji.
  4. Przygotuj test z wynikiem policzonym ręcznie.

8. Prawo Amdahla – od modelu do obliczenia

Utrwalającejednostkimodel danych

Program otrzymuje pierwotny czas wykonania T, procent części możliwej do zrównoleglenia oraz liczbę wątków N. Załóż, że T > 0, procent należy do przedziału [0, 100], a N >= 1. Przed napisaniem kodu wypisz dla każdej zmiennej jej znaczenie, typ, jednostkę i sensowny zakres. Następnie oblicz przyspieszenie oraz nowy czas.

S(N) = 1 / ((1 − p) + p / N)    oraz    Tnowy = T / S(N)
Rozwiązanie
#include <stdio.h>

int main(void) {
    double original_time, parallel_percent;
    int threads;
    scanf("%lf %lf %d", &original_time, &parallel_percent, &threads);

    double p = parallel_percent / 100.0;
    double speedup = 1.0 / ((1.0 - p) + p / threads);
    double parallel_time = original_time / speedup;

    printf("przyspieszenie: %.6f\n", speedup);
    printf("nowy czas:      %.6f\n", parallel_time);
    return 0;
}

Przypadek kontrolny: dla p = 0 przyspieszenie powinno wynosić 1 niezależnie od liczby wątków. Taki test wynika bezpośrednio z modelu, a nie z przypadkowego wyboru danych.


Audyt kodu wygenerowanego przez AI

AI otrzymało specyfikację zadania o transmisji pliku i zwróciło poniższy program. Kod wygląda krótko i przekonująco, ale nie realizuje modelu z zadania 7.

Znajdź błędy modelu i implementacji

AI-auditjednostkidzielenie całkowiteformat
#include <stdio.h>

int main(void) {
    int file_size_mb, bandwidth_mbps;
    scanf("%d %d", &file_size_mb, &bandwidth_mbps);

    double transmission_time = file_size_mb / bandwidth_mbps;
    printf("czas: %d s\n", transmission_time);
    return 0;
}

Znajdź co najmniej cztery problemy. Dla każdego określ, czy dotyczy specyfikacji, modelu matematycznego, typu danych czy sposobu wyświetlenia wyniku. Następnie podaj test, który ujawni problem bez zaglądania do rozwiązania.

Najważniejsze problemy: program nie przelicza bajtów na bity, pomija jednostki mega i narzut kontrolny, wykonuje dzielenie całkowite, a następnie przekazuje double do printf z formatem %d. Poprawna implementacja znajduje się już w rozwiązaniu zadania 7 – nie trzeba generować jej drugi raz.

Podsumowanie

Po tym laboratorium powinieneś patrzeć na wyrażenie nie tylko jak na zapis matematyczny, lecz jak na operację wykonywaną na wartościach określonych typów. To właśnie typ argumentów decyduje między innymi o rodzaju dzielenia, zakresie wyniku oraz wymaganym formacie wejścia i wyjścia.

Co naprawdę warto zapamiętać?

  • typ danych wpływa na dostępny zakres, precyzję i wynik operacji,
  • sizeof służy do sprawdzania rozmiaru, a nagłówki takie jak limits.h udostępniają granice typów,
  • format musi odpowiadać typowi argumentu,
  • dzielenie dwóch liczb całkowitych jest dzieleniem całkowitym,
  • % pomaga rozbijać wartości całkowite na większe i mniejsze jednostki,
  • rzutowanie może zmieniać wynik i powodować utratę informacji,
  • przed kodem warto zapisać dane, jednostki, wzór i przypadek kontrolny.

Kluczowa zasada: zanim zaakceptujesz wynik, ustal typ każdego istotnego podwyrażenia. Komputer nie wykonuje „wzoru w ogólności” – wykonuje konkretne operacje na konkretnych reprezentacjach danych.

Dalsza część pracy odbywa się na Platformie do ćwiczeń. Znajdziesz tam zadania utrwalające, które wykorzystują poznane narzędzia w nowych modelach i wzorach.

Pytania kontrolne

  1. Czym różni się wartość od jej formatu wyświetlania?
  2. Dlaczego nie należy zakładać jednego rozmiaru typu int na każdej platformie?
  3. Jakiego formatu użyjesz dla double w printf, a jakiego w scanf?
  4. Jaki wynik daje 7 / 3, a jaki 7.0 / 3?
  5. Dlaczego double x = 7 / 3; nie zachowuje części ułamkowej?
  6. Co robi operator % i dla jakich typów jest przeznaczony?
  7. Czy konwersja (int)-3.9 zaokrągla wartość?
  8. Po co może być potrzebna flaga -lm?
  9. Jaki test ujawni błąd dzielenia całkowitego podczas liczenia średniej?
  10. Jakie jednostki trzeba uzgodnić przed obliczeniem czasu transmisji?

Dodaj komentarz

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