Lekcja 2. Systemy liczbowe — dwójkowy, ósemkowy, szesnastkowy i kod U2
latwyPo co się tego uczymy?
Komputer "myśli" wyłącznie w zerach i jedynkach — każda liczba, znak i instrukcja w programie jest w pamięci zapisana binarnie. Bez zrozumienia systemów liczbowych trudno pojąć, dlaczego int ma ograniczony zakres, czym różni się liczba ujemna od dodatniej "w środku" komputera, ani jak działają operacje bitowe. To wymaganie egzaminacyjne (INF.04.2.5) i solidny fundament pod resztę kursu C#.
Teoria
System pozycyjny — wartość cyfry zależy od jej POZYCJI w liczbie, nie tylko od samego symbolu. W systemie dziesiętnym (podstawa 10, cyfry 0-9), z którego korzystamy na co dzień, liczba 245 to tak naprawdę 2·10² + 4·10¹ + 5·10⁰ = 200 + 40 + 5.
System dwójkowy (binarny, podstawa 2) — używa tylko cyfr 0 i 1. Każda pozycja to kolejna potęga liczby 2 (licząc od prawej: 1, 2, 4, 8, 16, 32...). Przykład: 1011₂ = 1·2³ + 0·2² + 1·2¹ + 1·2⁰ = 8 + 0 + 2 + 1 = 11₁₀.
Zamiana dziesiętny → binarny — dzielimy liczbę kolejno przez 2, zapisując reszty z dzielenia od DOŁU do GÓRY: 11 : 2 = 5 reszta 1; 5 : 2 = 2 reszta 1; 2 : 2 = 1 reszta 0; 1 : 2 = 0 reszta 1. Czytając reszty od ostatniej do pierwszej: 1011.
System ósemkowy (oktalny, podstawa 8) — cyfry 0-7, każda pozycja to potęga 8. Rzadziej używany dziś w praktyce, ale nadal spotykany (np. uprawnienia plików w Uniksie: chmod 755).
System szesnastkowy (heksadecymalny, podstawa 16) — cyfry 0-9 oraz A-F (gdzie A=10, B=11, C=12, D=13, E=14, F=15). Bardzo popularny w programowaniu, bo JEDNA cyfra szesnastkowa odpowiada DOKŁADNIE czterem bitom — dzięki temu zapis jest krótszy niż binarny, a przeliczenie proste. Przykłady zastosowań: kolory w CSS/HTML (#FF5733), adresy pamięci, zrzuty pamięci (hex dump). W C# liczby szesnastkowe zapisuje się z przedrostkiem 0x, np. 0xFF = 255.
Kod uzupełnieniowy do dwóch (U2) — sposób zapisu liczb UJEMNYCH w pamięci komputera. Bez niego procesor musiałby mieć osobną logikę dla dodawania i odejmowania — dzięki U2 odejmowanie to po prostu dodawanie liczby ujemnej, a sprzęt potrzebuje TYLKO jednego układu dodającego. Zasada tworzenia liczby ujemnej w U2 (dla 8 bitów): 1) zapisz liczbę dodatnią binarnie, 2) zneguj wszystkie bity (0→1, 1→0) — to tzw. kod uzupełnieniowy do jedynki (U1), 3) dodaj 1. Przykład dla -5 na 8 bitach: 5 = 00000101 → negacja → 11111010 → +1 → 11111011 = -5 w U2. Najstarszy (skrajnie lewy) bit pełni rolę bitu znaku: 0 = liczba dodatnia, 1 = liczba ujemna.
Dlaczego to działa — dodając liczbę i jej reprezentację U2 otrzymujemy zawsze zero (z pominięciem nadmiarowego bitu przeniesienia), np. 00000101 (5) + 11111011 (-5) = 100000000 → obcinając nadmiarowy 9. bit zostaje 00000000 = 0. Dokładnie to dzieje się "pod maską" w typie int w C#, gdy piszesz 5 + (-5).
Operacje bitowe — działania logiczne wykonywane bezpośrednio na pojedynczych bitach liczby: & (AND — 1 tylko gdy oba bity są 1), | (OR — 1 gdy choć jeden bit jest 1), ^ (XOR — 1 gdy bity są RÓŻNE), ~ (negacja bitowa — odwraca każdy bit), <</>> (przesunięcie bitowe w lewo/prawo — odpowiednik mnożenia/dzielenia przez potęgę 2).
Schemat
Liczba dziesiętna: 245
│
┌───────────────┼───────────────┐
▼ ▼ ▼
2·10² 4·10¹ 5·10⁰
= 200 = 40 = 5 → suma = 245
Liczba binarna: 1011
│
┌───────────┬─────────┼─────────┐
▼ ▼ ▼ ▼
1·2³ 0·2² 1·2¹ 1·2⁰
= 8 = 0 = 2 = 1 → suma = 11
Kod U2 dla -5 (8 bitów):
5 = 00000101
neguj → 11111010 (U1)
+1 → 11111011 (U2 = -5)
Przykład z życia
Kolory w CSS/HTML zapisujesz szesnastkowo (#3498DB) — dwie cyfry hex na kanał R, G, B (0-255 każdy). Adresy MAC kart sieciowych, klucze szyfrujące, sumy kontrolne plików (hash SHA-256) — wszystko to zapis szesnastkowy, bo jest zwarty i czytelny dla człowieka, a jednocześnie 1:1 odpowiada bitom w pamięci.
Program.cs
using System;
class SystemyLiczbowe
{
static void Main()
{
int liczba = 245;
// Konwersje wbudowane w .NET
string binarnie = Convert.ToString(liczba, 2);
string osemkowo = Convert.ToString(liczba, 8);
string szesnastkowo = Convert.ToString(liczba, 16);
Console.WriteLine($"Dziesiętnie: {liczba}");
Console.WriteLine($"Binarnie: {binarnie}");
Console.WriteLine($"Ósemkowo: {osemkowo}");
Console.WriteLine($"Szesnastkowo: {szesnastkowo.ToUpper()}");
// Zapis literału szesnastkowego wprost w kodzie
int kolorCzerwony = 0xFF0000;
Console.WriteLine($"0xFF0000 jako liczba dziesiętna: {kolorCzerwony}");
// Odczyt binarny -> int
int wynik = Convert.ToInt32("1011", 2);
Console.WriteLine($"1011 (binarnie) = {wynik} (dziesiętnie)");
// Operacje bitowe
int a = 12; // 1100
int b = 10; // 1010
Console.WriteLine($"a & b = {a & b}"); // 1000 = 8
Console.WriteLine($"a | b = {a | b}"); // 1110 = 14
Console.WriteLine($"a ^ b = {a ^ b}"); // 0110 = 6
Console.WriteLine($"a << 1 = {a << 1}"); // 11000 = 24 (mnożenie przez 2)
Console.WriteLine($"a >> 1 = {a >> 1}"); // 0110 = 6 (dzielenie całkowite przez 2)
}
}
Komentarz i wyjaśnienie kodu
Convert.ToString(liczba, podstawa) to gotowa funkcja biblioteczna .NET zamieniająca liczbę dziesiętną na zapis w innej podstawie (2, 8 lub 16) — w praktyce programista rzadko liczy to ręcznie, ale ZROZUMIENIE mechanizmu (dzielenie z resztą) jest wymagane na egzaminie teoretycznym.
0xFF0000 to literał szesnastkowy — kompilator C# rozpoznaje przedrostek 0x i sam przelicza wartość na liczbę całkowitą, dokładnie tak jak zwykłe liczby dziesiętne.
a << 1 (przesunięcie bitowe w lewo o 1) jest RÓWNOWAŻNE pomnożeniu przez 2 — a a >> 1 dzieleniu całkowitemu przez 2. Procesory wykonują przesunięcia bitowe znacznie szybciej niż mnożenie/dzielenie, więc bywają używane do optymalizacji (choć kompilator często robi to automatycznie).
Ćwiczenie samodzielne
Napisz program, który wczytuje liczbę dziesiętną od użytkownika i wypisuje jej odpowiedniki binarny, ósemkowy i szesnastkowy (użyj Convert.ToString). Następnie ręcznie (na kartce) zamień 3 wybrane liczby dziesiętne na binarne metodą dzielenia przez 2 i porównaj z wynikiem programu.
Zadania do pracy własnej
Napisz program zamieniający liczbę binarną (wpisaną jako tekst, np. "1101") na liczbę dziesiętną — bez używania
Convert.ToInt32, ręcznie licząc sumę potęg dwójki dla każdego bitu równego 1.Napisz metodę
DoU2(int liczba, int bitow), która dla liczby UJEMNEJ zwraca jej reprezentację w kodzie U2 jako tekst (np. dla -5 i 8 bitów zwraca "11111011"). Wskazówka: wykorzystaj operacje bitowe (negacja~i dodawanie 1) alboConvert.ToStringna odpowiednio przygotowanej liczbie.Napisz program symulujący "kalkulator bitowy": użytkownik podaje dwie liczby całkowite oraz wybiera operację (AND, OR, XOR, przesunięcie w lewo/prawo), a program wypisuje wynik zarówno dziesiętnie, jak i binarnie (z wyrównaniem do tej samej liczby bitów, np. przez
PadLeft), żeby wizualnie było widać, które bity się zmieniły.
Typowe błędy
Mylenie kolejności reszt przy ręcznej zamianie dziesiętny→binarny — reszty z kolejnych dzieleń trzeba odczytać OD OSTATNIEJ DO PIERWSZEJ (od dołu do góry), a nie w kolejności, w jakiej zostały policzone.
Zapominanie o bicie znaku w U2 — najstarszy bit w kodzie U2 NIE jest zwykłą cyfrą wagi 2ⁿ⁻¹, tylko wskazuje znak liczby (0=dodatnia, 1=ujemna); traktowanie go jak zwykłego bitu przy ręcznym liczeniu wartości prowadzi do błędnego wyniku.
Mylenie operatorów bitowych &/| z operatorami logicznymi &&/|| — te pierwsze działają na POJEDYNCZYCH BITACH liczby, te drugie na wartościach logicznych bool (prawda/fałsz) w warunkach if. Użycie & zamiast && w warunku zwykle nadal "działa" (bo C# potrafi niejawnie to obsłużyć dla typu bool), ale to zupełnie inna, mniej czytelna operacja.
Nawiązanie do egzaminu zawodowego
To wymaganie INF.04.2.5 — jedna z niewielu kwestii z "Podstaw informatyki" (INF.04.2), która ma bezpośrednie zastosowanie w programowaniu i dlatego znalazła się w głównym torze kursu, nie w dziale pobocznym. Zrozumienie reprezentacji binarnej ułatwia też późniejsze zrozumienie zakresów typów liczbowych w C# (np. dlaczego int mieści maksymalnie ok. 2,1 miliarda) oraz operacji bitowych używanych czasem w optymalizacji kodu i algorytmach szyfrowania (lekcja 23).