Serwis Edukacyjny Nauczycieli w I-LO w Tarnowie Materiały dla uczniów liceum |
Wyjście Spis treści Wstecz Dalej
Autor artykułu: mgr Jerzy
Wałaszek |
©2024 mgr Jerzy Wałaszek
|
SPIS TREŚCI |
Podrozdziały |
Bit jest najmniejszą jednostką informacji, którą przetwarza komputer. Nie wnikając w skomplikowane teorie informatyczne, możemy powiedzieć, że bit jest "takim czymś", co może występować tylko w dwóch postaciach. Co to jest "to coś"? Otóż "to coś" zależy od sposobu realizacji bitu w danym systemie. Współczesne komputery w swoich wnętrzach przedstawiają bity w postaci napięć (o napięciu elektrycznym powinieneś się uczyć na fizyce, jeśli nie, to kup sobie baterię paluszkową, a otrzymasz wzorzec napięcia elektrycznego o wartości około 1,5V). Napięcia są wygodną postacią bitów dla układów elektronicznych. Dlatego taką reprezentację bitów wybrano. W przyszłości w komputerach kwantowych bity będą zapewne kodowane stanami kwantowymi atomów, ale to jeszcze daleka przyszłość.
Aby bit kodować napięciem, musimy przyjąć dwie wartości tego napięcia, ponieważ bit może znajdować się w dwóch stanach. I tak:
W sumie jest to sprawa techniczna i nas mało obchodzi. W informatyce bity zwykle oznacza się cyframi: 0 – stan niski i 1 – stan wysoki. Sama nazwa bit pochodzi od angielskiego terminu binary digit, czyli cyfra dwójkowa, tzn. cyfra 0 lub 1.
Jeśli bity potraktujemy jak cyfry, to za ich pomocą można zapisywać liczby.
Setki lat temu pewien hinduski mędrzec wymyślił stosowany przez do dzisiaj system liczbowy zwany pozycyjnym systemem dziesiętnym. Takie systemy wymyślały już wcześniejsze cywilizacje, ale hinduski jest z nich najbardziej zaawansowany.
Dowolną liczbę zapisuje się skończoną ilością znaków, czyli cyfr. W systemie dziesiętnym tych znaków jest 10:
0 1 2 3 4 5 6 7 8 9
Znasz je dobrze i są dla ciebie zupełnie naturalne. Cechą charakterystyczną systemu pozycyjnego jest to, iż cyfry reprezentują różne wartości w zależności od swojej pozycji w zapisie liczby. Na przykład: liczba 555 składa się z trzech takich samych cyfr 555. Ale pierwsza cyfra 5 oznacza pięć setek, druga cyfra 5 oznacza pięć dziesiątek, a ostatnia oznacza pięć jednostek. Te setki, dziesiątki i jednostki to są tzw. wagi pozycji. Każda pozycja w liczbie posiada swoją wagę, która jest potęgą liczby 10, a liczba 10 jest tzw. podstawą systemu:
wagi | 1000 103 |
100 102 |
10 101 |
1 100 |
liczba | 5 | 5 | 5 | 5 |
pozycje | 3 | 2 | 1 | 0 |
Pozycje numerujemy od 0 od strony prawej ku lewej. Zwróć uwagę, że waga pozycji to podstawa podniesiona do potęgi o wykładniku równym numerowi pozycji. Taki sposób zapisu liczb umożliwia zapis dowolnej wartości. Potrzebujemy większej liczby, dodajemy kolejną pozycję o wadze 10 razy większej, itd.
Wartość liczby uzyskasz mnożąc cyfry przez wagi pozycji, na których stoją, a następnie sumując te iloczyny. Dla liczby dziesiętnej nie odkryjemy nic nowego, ale zapiszmy:
wagi |
1000 103 |
100 102 |
10 101 |
1 100 |
|
liczba | 2 | 6 | 4 | 3 | = 2 x 103 + 6 x 102 + 4 x 101 + 1 x 100 |
pozycje | 3 | 2 | 1 | 0 |
Pięknym jest to, że system pozycyjny wcale nie musi opierać się na podstawie 10. Można wybrać inną liczbę. Na przykład weźmy za podstawę systemu liczbę 4. W każdym systemie pozycyjnym cyfr jest tyle, ile wynosi jego podstawa. Dla systemu czwórkowego mamy tylko 4 cyfry: 0 1 2 3. Wagi pozycji są teraz potęgami liczby 4, a nie dziesięć. To się zmienia. Ale zasada wyliczania wartości pozostaje taka sama, jak w systemie dziesiętnym:
wagi |
64 43 |
16 42 |
4 41 |
1 40 |
|
liczba | 2 | 3 | 2 | 1 | = 2 x 43 + 3 x 42 + 2 x 41 + 1 x 40 = 2 x 64 + 3 x 16 + 2 x 4 + 1 x 1 = 249 |
pozycje | 3 | 2 | 1 | 0 |
Jeśli za podstawę przyjmiemy liczbę 2, to powstanie system dwójkowy. W systemie dwójkowym są dwie cyfry: 0 i 1. Wagi pozycji są potęgami liczby 2. Zasada obliczania wartości wciąż pozostaje taka sama:
wagi |
8 23 |
4 22 |
2 21 |
1 20 |
|
liczba | 1 | 1 | 0 | 1 | = 1 × 8 + 1 × 4 + 0 × 2 + 1 × 1 = 13 |
pozycje | 3 | 2 | 1 | 0 |
Doszliśmy do systemu zapisu liczb, w którym na cyfry można użyć bitów, czyli za pomocą bitów możemy zapisać dowolne liczby, bo to jest wspólna cecha wszystkich systemów pozycyjnych. Wadą z punktu widzenia człowieka jest to, że liczby większe mają długi zapis i łatwo się pomylić: 11110101010100111110102. Ale system dwójkowy nie jest przeznaczony dla dla ludzi, tylko dla komputerów, a im pasuje doskonale.
Tak zdefiniowany system dwójkowy pozwala zapisywać liczby całkowite nieujemne. W informatyce nazywa się on Naturalnym Kodem Dwójkowym (ang. NBC = Natural Binary Code).
Pamięć komputerowa, która przechowuje dane, jest zorganizowana w komórki przechowujące po 8 bitów. To rozwiązanie przyjęto z powodów ekonomicznych. 8 bitów to tzw. bajt (ang. byte).
Jeden bajt może przyjmować wartości od 000000002 do 111111112, czyli od 0 do 255 w systemie dziesiętnym:
111111112 = 1 × 27 + 1 × 26
+ 1 × 25 + 1 × 24 + 1 × 23 + 1
× 22 + 1 × 21 + 1 × 20
111111112 = 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1
111111112 = 255
W języku C++ daną jednobajtową reprezentuje się typem unsigned char. Od wersji 11 C++ pojawił się wydzielony typ uint8_t (ang. uint = unsigned integer).
Uruchom CodeBlocks, utwórz projekt konsoli, wpisz poniższy program i wciśnij F9, aby go skompilować i uruchomić:
C++// Typy 1-bajtowe //--------------- #include <inttypes.h> #include <iostream> using namespace std; int main() { cout << sizeof(unsigned char) << endl << sizeof(uint8_t) << endl; return 0; } |
Program wyświetla rozmiar danych w bajtach. Otrzymasz 2 jedynki. Typ unsigned char jest używany standardowo do reprezentacji znaków o kodach ASCII od 0 do 255. Typ uint8_t używa się do liczb o wartościach od 0 do 255. W sumie to to samo.
Uruchom kolejny program:
C++// Typy 1-bajtowe //--------------- #include <inttypes.h> #include <iostream> using namespace std; int main() { unsigned char a = 65; uint8_t b = 65; cout << a << endl << b << endl; return 0; } |
W programie tworzymy dwie zmienne. Zmienna a jest typu unsigned char. Zmienna b jest typu uint8_t. Obu zmiennym nadajemy tę samą wartość 65. W wyniku otrzymujemy dwie litery A, ponieważ liczba 65 jest kodem ASCII właśnie litery A. Oznacza to, iż z punktu widzenia kompilatora oba typy unsigned char i uint8_t to ten sam typ znakowy. Oczywiście w niczym to nie przeszkadza, bo znak to tak naprawdę kod, czyli liczba całkowita.
Gdy programujesz z biblioteką SDL2, to otrzymujesz dodatkową definicję tego typu: Uint8.
Zamknij bieżący projekt i utwórz nowy projekt sdl2.
Wpisz poniższy program, skompiluj go i uruchom :
C++// Typy 1-bajtowe //---------------- #include <SDL.h> #include <iostream> using namespace std; int main(int argc, char* args[]) { Uint8 a = 65; cout << sizeof(a) << endl << a << endl; return 0; } |
Program tworzy zmienną a typu Uint8 i nadaje jej wartość 65. Następnie wyświetlamy rozmiar w bajtach zmiennej a oraz jej zawartość. Otrzymasz 1 i A. Wynika z tego, że typ Uint8 jest po prostu kolejnym synonimem typu unsigned char.
Po co nam nowy typ, skoro mamy już unsigned char? Może dlatego, że krócej się pisze Uint8 i jest bardziej czytelne (Unsigned integer 8 bits)? Nie, chodzi tutaj o kompatybilność i ujednolicenie typów. SDL2 może być zainstalowane na różnych platformach sprzętowych, nie tylko na komputerach PC. Stosowanie typu Uint8 daje nam pewność, że zawsze będzie to 8 bitów w naturalnym kodzie dwójkowym.
W SDL2 dostępne są jeszcze typy Uint16 (16 bitów), Uint32 (32 bity), Uint64 (64 bity).
Podsumujmy:
Standard C++ | C++ 11 | SDL2 | Zakres |
unsigned char | uint8_t | Uint8 | 0...255 |
unsigned short int | uint16_t | Uint16 | 0...65535 |
unsigned int | uint32_t | Uint32 | 0...4294967295 |
unsigned long long int | uint64_t | Uint64 | 0...18446744073709551615 |
Zakres dowolnego typu w kodzie NBC o n bitach obliczysz ze wzoru: 0...2n-1.
W projekcie SDL2 podmień program na poniższy:
C++// Typy całkowite //---------------- #include <SDL.h> #include <iostream> using namespace std; int main(int argc, char* args[]) { cout << "TYPY DANYCH W SDL2\n" "------------------\n\n" << "Uint8 ROZMIAR: " << sizeof(Uint8) << endl << "Uint16 ROZMIAR: " << sizeof(Uint16) << endl << "Uint32 ROZMIAR: " << sizeof(Uint32) << endl << "Uint64 ROZMIAR: " << sizeof(Uint64) << endl; return 0; } |
C++// Typy całkowite //---------------- #include <SDL.h> #include <iostream> using namespace std; int main(int argc, char* args[]) { Uint16 a = -5; cout << a << endl; return 0; } |
Powyższy program daje wynik 65531. Dlaczego tak się dzieje, zobaczysz za chwilę.
Powstaje problem: jak kodować liczby ujemne. W systemie dziesiętnym po prostu wymyślono znak minus, który dopisujemy przed liczbą: np. -45. Ale znak minus to dodatkowy symbol. Komputery przetwarzają wyłącznie bity, czyli cyfry 0 i 1. Dlatego wymyślono kilka sposobów kodowania liczb ujemnych. Najpopularniejszy jest system uzupełnień do 2, zwany krótko U2 (ang. Two's Complement). Musimy tutaj wprowadzić umowę, że liczba U2 posiada ustaloną z góry liczbę bitów. To ważne, ponieważ najstarszy bit ma wagę ujemną. Weźmy dla przykładu 4-bitowe liczby U2:
wagi |
(-8) (-23) |
4 22 |
2 21 |
1 20 |
|
liczba | 1 | 1 | 0 | 1 | = 1 × (-8) + 1 × 4 + 0 × 2 + 1 × 1 = -3 |
pozycje | 3 | 2 | 1 | 0 |
Wartość liczby U2 obliczamy tak samo jak dla liczb NBC: mnożymy cyfry przez wagi ich pozycji i sumujemy te iloczyny. Wagi pozycji są potęgami podstawy 2. Waga najstarszej pozycji jest ujemna. Najstarszy bit zwany jest bitem znaku (ang. sign bit). Jeśli ma wartość 1, to liczba jest ujemna, jeśli ma wartość 0, to liczba jest nieujemna. W poniższej tabelce są wszystkie wartości 4-bitowych liczb U2:
(-8) (-23) |
4 22 |
2 21 |
1 20 |
|
0 | 0 | 0 | 0 | = 0 |
0 | 0 | 0 | 1 | = 1 |
0 | 0 | 1 | 0 | = 2 |
0 | 0 | 1 | 1 | = 3 (2 + 1) |
0 | 1 | 0 | 0 | = 4 |
0 | 1 | 0 | 1 | = 5 (4 + 1) |
0 | 1 | 1 | 0 | = 6 (4 + 2) |
0 | 1 | 1 | 1 | = 7 (4 + 2 + 1) |
1 | 0 | 0 | 0 | = -8 |
1 | 0 | 0 | 1 | = -7 (-8 + 1) |
1 | 0 | 1 | 0 | = -6 (-8 + 2) |
1 | 0 | 1 | 1 | = -5 (-8 + 2 + 1) |
1 | 1 | 0 | 0 | = -4 (-8 + 4) |
1 | 1 | 0 | 1 | = -3 (-8 + 4 + 1) |
1 | 1 | 1 | 0 | = -2 (-8 + 4 + 2) |
1 | 1 | 1 | 1 | = -1 (-8 + 4 + 2 + 1) |
Zwróć uwagę, że wartości ujemnych jest o 1 więcej od wartości dodatnich (zero nie ma znaku).
Zakres n-bitowych liczb U2 liczymy wzorem: -2n-1...2n-1-1.
Na przykład 8 bitowe liczby U2 mają zakres: od
Dlaczego stosuje się taki dziwny kod U2? Powodem jest to, iż obliczenia na liczbach U2 można wykonywać w tych samych układach elektronicznych co obliczenia na liczbach NBC. Zatem procesor nie musi zawierać osobnych obwodów dla liczb ze znakiem i bez znaku. W efekcie mamy oszczędność.
Daną grupę bitów można interpretować zarówno jako liczbę ze znakiem jak i bez znaku. Jednak wartości mogą wtedy być inne:
Kod | Wartość | |
NBC | U2 | |
0000 | 0 | 0 |
0001 | 1 | 1 |
0010 | 2 | 2 |
0011 | 3 | 3 |
0100 | 4 | 4 |
0101 | 5 | 5 |
0110 | 6 | 6 |
0111 | 7 | 7 |
1000 | 8 | -8 |
1001 | 9 | -7 |
1010 | 10 | -6 |
1011 | 11 | -5 |
1100 | 12 | -4 |
1101 | 13 | -3 |
1110 | 14 | -2 |
1111 | 15 | -1 |
Jeśli bit znaku ma wartość 0 (czyli liczba jest nieujemna, dodatnia), to oba kody przedstawiają tę samą liczbę. Gdy bit znaku przyjmuje wartość 1 (liczba staje się ujemna), liczby NBC i U2 różnią się między sobą. Zwróć jednak uwagę, że występuje wtedy pomiędzy nimi prosty związek:
Liczba NBC = 16 + Liczba U2
Dla n bitowych liczb otrzymamy:
Liczba NBC = 2n + Liczba U2
W programie przedstawionym na początku tego podrozdziału w zmiennej bezznakowej umieściliśmy wartość U2 równą -5. Zmienna miała długość 16 bitów, zatem:
Wartość NBC = 216 - 5 = 65536 - 5 = 65531
Po prostu komputer umieścił liczbę U2 w zmiennej NBC, a później zinterpretował tę liczbę jako liczbę NBC i wyszedł mu wynik 65531. Czy teraz jest to dla ciebie jasne? To jest prosta konsekwencja podwójnej interpretacji liczb. Musisz na takie rzeczy uważać w swoich programach, bo czasem prowadzą do trudno wykrywalnych błędów.
Mamy następujące typy zmiennych całkowitych ze znakiem (ang. Sint = Signed integer):
Standard C++ | C++ 11 | SDL2 | Zakres |
signed char | sint8_t | Sint8 | -128...127 |
signed short int | sint16_t | Sint16 | -32768...32767 |
signed int | sint32_t | Sint32 | -2147483648...2147483647 |
signed long long int | sint64_t | Sint64 | -9223372036854775808...9223372036854775807 |
W projekcie SDL2 uruchom poniższy program:
C++// Typy całkowite ze znakiem //-------------------------- #include <SDL.h> #include <iostream> using namespace std; int main(int argc, char* args[]) { cout << "TYPY DANYCH W SDL2\n" "------------------\n\n" << "Sint8 ROZMIAR: " << sizeof(Sint8) << endl << "Sint16 ROZMIAR: " << sizeof(Sint16) << endl << "Sint32 ROZMIAR: " << sizeof(Sint32) << endl << "Sint64 ROZMIAR: " << sizeof(Sint64) << endl; return 0; } |
Jeśli cię zainteresował temat typów danych, to przeczytaj nasz artykuł o binarnym kodowaniu liczb, który dokładniej opisuje to zagadnienie.
Stała | Wartość | Uwagi |
SDL_MAX_SINT8 | 127 | Graniczne wartości liczb 8-bitowych ze znakiem |
SDL_MIN_SINT8 | -128 | |
SDL_MAX_UINT8 | 255 | Graniczne wartości liczb 8-bitowych bez znaku |
SDL_MIN_UINT8 | 0 | |
SDL_MAX_SINT16 | 32767 | Graniczne wartości liczb 16-bitowych ze znakiem |
SDL_MIN_SINT16 | -32768 | |
SDL_MAX_UINT16 | 65535 | Graniczne wartości liczb 16-bitowych bez znaku |
SDL_MIN_UINT16 | 0 | |
SDL_MAX_SINT32 | 2147483647 | Graniczne wartości liczb 32-bitowych ze znakiem |
SDL_MIN_SINT32 | -2147483648 | |
SDL_MAX_UINT32 | 4294967295 | Graniczne wartości liczb 32-bitowych bez znaku |
SDL_MIN_UINT32 | 0 | |
SDL_MAX_SINT64 | 9223372036854775807 | Graniczne wartości liczb 64-bitowych ze znakiem |
SDL_MIN_SINT64 | -9223372036854775808 | |
SDL_MAX_UINT64 | 18446744073709551615 | Graniczne wartości liczb 64-bitowych bez znaku |
SDL_MIN_UINT64 | 0 |
W projekcie SDL2 wpisz poniższy program, skompiluj go i uruchom:
C++// Typy całkowite //--------------- #include <SDL.h> #include <iostream> using namespace std; int main(int argc, char* args[]) { cout << "ZAKRESY DANYCH W SDL2\n" "---------------------\n\n" << "Sint8 : " << (int)SDL_MIN_SINT8 << " ... " << (int)SDL_MAX_SINT8 << endl << "Uint8 : " << (int)SDL_MIN_UINT8 << " ... " << (int)SDL_MAX_UINT8 << endl << "Sint16 : " << SDL_MIN_SINT16 << " ... " << SDL_MAX_SINT16 << endl << "Uint16 : " << SDL_MIN_UINT16 << " ... " << SDL_MAX_UINT16 << endl << "Sint32 : " << SDL_MIN_SINT32 << " ... " << SDL_MAX_SINT32 << endl << "Uint32 : " << SDL_MIN_UINT32 << " ... " << SDL_MAX_UINT32 << endl << "Sint64 : " << SDL_MIN_SINT64 << " ... " << SDL_MAX_SINT64 << endl << "Uint64 : " << SDL_MIN_UINT64 << " ... " << SDL_MAX_UINT64 << endl; return 0; } |
W pojedynczej komórce pamięci można umieścić daną typu char, Uint8 lub Sint8. Dane o długości większej niż 8 bitów wymagają więcej niż jednej komórki pamięci. Na przykład w CodeBlocks mamy:
Typ | Liczba komórek |
Uint16 | 2 |
Uint32 | 4 |
Uint64 | 8 |
Porcja 8 bitów nazywana jest bajtem (ang. byte). Jeśli dane o długości większej niż 8 bitów podzielimy na bajty, to w każdym z nich otrzymamy różne bity danej. Na przykład dla Uint16 mamy:
bity | b15 | b14 | b13 | b12 | b11 | b10 | b9 | b8 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 |
bajty | b15 ... b8 | b7 ... b0 | ||||||||||||||
oznaczenia | starszy bajt MSB | młodszy bajt LSB |
Bajt zawierający starsze bity danej nazywamy starszym lub bardziej znaczącym bajtem MSB (ang. More Significant Byte). Bajt zawierający młodsze bity danej nazywamy młodszym lub mniej znaczącym bajtem LSB (ang. Less Significant Byte). Kolejność tych bajtów w pamięci zależy od rodzaju mikroprocesora komputera. Stosowane są dwa rozwiązania:
Big Endian - bajt MSB jest pierwszy, procesory Motorola 68k (komputery Macintosh).
Little Endian - bajt LSB jest pierwszy, procesory Intel Pentium i kompatybilne (komputery IBM PC).
Na przykład dana Uint16 (dwójkowo) o wartości 11111111000000002 zostanie umieszczona w pamięci jako:
Big Endian | Little Endian | |||||||||||||||||||||||||||||||||
|
|
Jak widzisz, kolejność bajtów jest odwrotna. To samo dotyczy danych o większej liczbie bajtów: w trybie Big Endian zapis rozpoczyna się od najstarszych bajtów, a w trybie Little Endian od najmłodszych.
Jak sprawdzić, który system jest stosowany na twoim komputerze? Bardzo prosto. Utwórz w CodeBlocks projekt sdl2, przekopiuj poniższy program, skompiluj go i uruchom:
C++// Little/Big Endian //------------------ #include <SDL.h> #include <iostream> using namespace std; int main(int argc, char* args[]) { Uint16 a = 1; Uint8 * p = (Uint8 *) & a; if( * p == 1) cout << "Little Endian" << endl; else cout << "Big Endian" << endl; return 0; } |
Program tworzy zmienną a typu Uint16, czyli zajmującą w pamięci RAM 2 bajty. Następnie tworzy wskaźnik p do danych jednobajtowych typu Uint8. We wskaźniku p umieszcza adres pierwszego bajtu zmiennej a. Zwróć uwagę, iż należy tu zastosować rzutowanie, czyli informujemy kompilator, aby potraktował adres zmiennej Uint16 tak, jakby to był adres zmiennej Uint8. W ten sposób uzyskujemy dostęp do pierwszego bajtu zmiennej a. Instrukcja warunkowa if sprawdza, czy w pierwszym bajcie znajduje się wartość 1. Jeśli tak, to jest to młodszy bajt LSB zmiennej, a zatem mamy do czynienia z trybem Little Endian. W przeciwnym razie pierwszym bajtem jest MSB zmiennej, czyli mamy do czynienia z trybem Big Endian.
Zwykle tryby te nie muszą zaprzątać twojej uwagi, o ile nie musisz operować w pamięci na poszczególnych bajtach danej. Jednak czasami występuje konieczność konwersji, jeśli musimy wczytać do naszego programu dane utworzone i zapisane w systemie Big Endian, a zatem odwrotnym do tego, który stosują komputery IBM PC.
Napiszmy prostą funkcję, która odwraca kolejność bajtów w zmiennej. Utwórz w CodeBlocks projekt sdl2, przekopiuj poniższy program, skompiluj go i uruchom:
C++// Little/Big Endian //------------------ #include <SDL.h> #include <iostream> using namespace std; // Funkcja odwraca kolejność bajtów // p - wskaźnik zmiennej // n - liczba bajtów void ReverseBytes(void * p, int n) { Uint8 * p1, * p2, x; p1 = (Uint8 *) p; p2 = p1 + n - 1; while(p1 < p2) { x = * p1; * p1 = * p2; * p2 = x; p1++; p2 --; } } int main(int argc, char* args[]) { Uint64 a = 0x0102030405060708L; cout << "Przed: 0" << hex << a << endl; ReverseBytes(&a,8); cout << "Po: 0" << hex << a << endl; return 0; } |
Zespół Przedmiotowy Chemii-Fizyki-Informatyki w I Liceum Ogólnokształcącym im. Kazimierza Brodzińskiego w Tarnowie ul. Piłsudskiego 4 ©2024 mgr Jerzy Wałaszek |
Materiały tylko do użytku dydaktycznego. Ich kopiowanie i powielanie jest dozwolone
pod warunkiem podania źródła oraz niepobierania za to pieniędzy.
Pytania proszę przesyłać na adres email:
Serwis wykorzystuje pliki cookies. Jeśli nie chcesz ich otrzymywać, zablokuj je w swojej przeglądarce.
Informacje dodatkowe.