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 albodoubledo 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
printfiscanf, - 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ęcie | Pytanie kontrolne | Przykład |
| Wartość | Co chcemy przechować? | 42 |
| Typ | Jak program ma interpretować dane? | int |
| Reprezentacja | Jak wartość jest zakodowana w pamięci? | ciąg bitów |
| Format | Jak 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,052albo0x2A. Zmiana sposobu wyświetlania nie musi oznaczać zmiany wartości przechowywanej przez program.
1. Dobór typu do problemu
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.
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.
| Typ | Typowe zastosowanie | Przykładowa wartość |
int | liczniki, indeksy, małe liczby całkowite | -12, 0, 2026 |
unsigned int | wartości nieujemne i operacje bitowe | 42u |
long long | większe liczby całkowite | 9000000000LL |
char | pojedyncze znaki i małe jednostki danych | 'A' |
float | liczby zmiennoprzecinkowe o mniejszej precyzji | 3.5f |
double | domyś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?
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.
| Typ | printf | scanf |
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. Wprintfużywamy%f, natomiast wscanf–%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
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
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.
#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łkowite5 / 2, a dopiero wynik2zostaje zapisany jako2.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
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.
#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żenie | Typ operacji | Wynik |
5 / 2 | dzielenie całkowite | 2 |
5.0 / 2 | dzielenie zmiennoprzecinkowe | 2.5 |
(double)5 / 2 | dzielenie zmiennoprzecinkowe | 2.5 |
5 % 2 | reszta z dzielenia | 1 |
Operator % działa na typach całkowitych i zwraca resztę z dzielenia. Dla niezerowego b zachodzi zależność:
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
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.
#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;
}
3725 → 01:02:0560 → 00:01:000 → 00:00:007. Czas transmisji pliku
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óworaz1 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.
#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:
- Nazwij dane i ich jednostki.
- Zapisz wzór albo pseudokod.
- Dobierz typy i kolejność operacji.
- Przygotuj test z wynikiem policzonym ręcznie.
8. Prawo Amdahla – od modelu do obliczenia
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.
#include <stdio.h>
int main(void) {
double original_time, parallel_percent;
int threads;
scanf("%lf %lf %d", &original_time, ¶llel_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
#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
doubledoprintfz 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,
sizeofsłuży do sprawdzania rozmiaru, a nagłówki takie jaklimits.hudostę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
- Czym różni się wartość od jej formatu wyświetlania?
- Dlaczego nie należy zakładać jednego rozmiaru typu
intna każdej platformie? - Jakiego formatu użyjesz dla
doublewprintf, a jakiego wscanf? - Jaki wynik daje
7 / 3, a jaki7.0 / 3? - Dlaczego
double x = 7 / 3;nie zachowuje części ułamkowej? - Co robi operator
%i dla jakich typów jest przeznaczony? - Czy konwersja
(int)-3.9zaokrągla wartość? - Po co może być potrzebna flaga
-lm? - Jaki test ujawni błąd dzielenia całkowitego podczas liczenia średniej?
- Jakie jednostki trzeba uzgodnić przed obliczeniem czasu transmisji?