Witam. Od jakiegoś czasu zajmuję się pisaniem kodu w różnych językach programowania. Koncentruję się zazwyczaj na języku PHP, głównie z powodu jego 'wysokości' w klasie języków C–podobnych. Znaczy to tyle, że daje mi dostatecznie dużo narzędzi tak, abym nie musiał wszystkiego budować od zera. W ten sposób mogę się skoncentrować na robieniu tego, co chcę zrobić. Oczywiście, wszystko ma swoje plusy i minusy – wysoka abstrakcja języka PHP sprawia, że jest on dość wolny w porównaniu np. z językiem C czy C++ (PHP jest językiem skryptowym, choć o wiele mniej zasobożernym od Javy). Co nie znaczy, że PHP jest powolnym językiem. C jest po prostu szybszy, gdyż jest 'bliżej maszyny', a z PHP trzeba najpierw 'przetłumaczyć' na język komputera – wprowadza to opóźnienia.
Ale ja nie o tym chciałem. W ciągu tych kilku lat, które spędziłem głównie na programowaniu, spotkałem nieco osób które brały się za to, nie znając nawet terminologii. Jako, że po prostu nie chce mi się wyjaśniać czegoś
n–ty raz, spiszę niektóre ważniejsze fakty i własności w niniejszych artykułach. Zacznę od typów danych, ale nietypowo. ;–)
W PHP jest taka kochana funkcja – pack() (oraz jej siostra, unpack()). Funkcja pack() służy do 'pakowania' zmiennych w bit–string. Istnieje głównie dlatego, że język PHP jest 'beztypowy' (przynajmniej na powierzchni), i znajduje szereg genialnych zastosowań. Wyjaśnienie funkcji pack() i jej formatów na stronie manuala PHP (php.net/manual) jest całkiem przejrzyste i przystępne, niestety – nie dla wszystkich. Zacznijmy więc od prostych definicji.
- Typy podstawowe (zauważ: wszystkie typy podstawowe i rozszerzone poza float/double przechowują wartości stałoprzecinkowe (całkowite)!)
- bit – najbardziej bazowa, najmniejsza jednostka informacji w komputerze. Jeden bit (małe 'b') potrafi przechowywać tylko dwie wartości: 0 oraz 1 (odpowiada to kolejno logicznemu "prawda" (TRUE) oraz "fałsz" (FALSE)). Kiedy piszę, że coś ma 'wielkość' (lub 'długość') np. 42b, oznacza to rząd 42 bitów.
- nibble – 4 bity, jeden za drugim. Nibble (lub nybble) to 'pół bajta', i określenie to jest stosowane głównie w powiązaniu z notacją szesnastkową.
- bajt (byte) – 8 bitów. Bajt (duże 'B') to 'standardowa' jednostka przechowywania informacji, i zasadniczo – najbardziej uniwersalna. W tym momencie powinno już dla Ciebie, czytelniku, sens mieć stwierdzenie "1B = 8b". Bajt także używany jest do określania wielkości innych typów.
- float oraz double – wartość zmiennoprzecinkowa – jej konstrukcja w pamięci komputera zależy od WIELU czynników (platforma, architektura, język programowania etc.). Float może mieć wielkość 16, 24 i 32bit, a double – 32, 48 i 64bit. Zasada działania jest prosta – wartości do umówionego bita traktuje się jak część całkowitą, a po tej umówionej pozycji – jako ułamek całości. Dla przykładu, jeśli umówimy się że 'minifloat' ma wielkość 2 bitów (2 pozycje), i pierwsza pozycja określa całości a druga ułamek, wartość 0.5 możemy zapisać jako '01', 1 = '10', a 1.5 = '11'. W tym przypadku mowa o 'dokładności' – czyli: ile miejsc po przecinku możemy zachować, oraz o zaokrąglaniu 'w górę' lub 'w dół'. Wartości są zazwyczaj zaokrąglane 'w dół'.
- Pozycyjność
System binarny (dwójkowy), tak samo jak system dziesiętny (decymalny) – jest pozycyjny. Tak samo, jak w systemie dziesiętnym 12 nie równa się 21, tak w binarnym 01 nie jest równie 10. W systemie pozycyjnym miejsce danej cyfry w liczbe symbolizuje potęgę podstawy systemu – na przykład:
10 = 1 * 10 ^1 + 0 * 10 ^0 = 1*10 + 0*1
120 = 1 * 10^2 + 2 * 10^1 + 0 *10^0 = 1*100 + 2*10 + 0*1
Dobrym przykładem systemu niepozycyjnego jest rzymski system zapisywania liczb. Na przykład w zapisie złożonym ze znaków I oraz V, z kombinacji tych dwu możemy uzyskać IV (4) lub VI (6), ale I wciąż oznacza 1 a V = 5, niezależnie od ich pozycji (owszem – liczy się kolejność, ale to nie znaczy – pozycyjność). Gdy w systemie rzymskim II oznacza 2, w dziesiętnym 11 nie oznacza 2. Mam nadzieję, że zrozumiałe... ;–)
Wartości binarne przelicza się na dziesiętne dość prosto. Dla przykładu, wyrażenie 1001 rozkłada się od prawej do lewej, w taki sposób:
1* 2^0 + 0* 2^1 + 0* 2^2 + 1* 2^3 = 1 + 0 + 0 + 8 = 9
Dodam od razu, że wartość 1001 (9) ma długość 4 bitów, i wszystko do tego miejsca powinno być jasne.
- Typy 'rozszerzone' (note: używam nazwy 'rozszerzone', gdyż są jakby pochodną podstawowych, a nie są to typy 'złożone', które są osobną kategorią)
- integer (int) – i tu zaczynają się schody. W zależności od platformy sprzętowej (Intel, Motorola etc.) wielkość INTa może być różna. Zasadniczo w systemach 32–bitowych przyjmuje się, że INT ma wielkość 16 bitów (czyli 2B – 2 bajty).
- word (słowo) lub 'long int' – wielkość tego typu trzeba sobie wyekstrapolować z wielkości INTa – word (long int) zawsze będzie mieć wielkość 2x int. W systemach 32–bitowych przyjmuje się, że word ma wielkość 32–bit (4B).
- Typy złożone
- array (tablica) – tablice są cudowną konstrukcją – przechowują określoną ilość wartości tego samego typu (lub różnego, w PHP). Dla przykładu, można mieć tablicę, w której przechowuje się wiek kilku osób.
- string (dosł. linka) – specjalny przypadek tablicy złożonej z bajtów interpretowanych jako znaki tekstowe. W różnych językach programowania string może być zakończony znakiem NULL (0), spacją (20) lub być ograniczony do konkretnej wielkości (tzw. PASCAL string). We wszystkich trzech przypadkach string będzie tablicą typu byte, o wielkości o jeden większej niż ciąg tekstowy – na przykład słowo "wyraz" 'zmieści się' w tablicy 6–elementowej. [NOTE: w PHP stringi są obsługiwane o poziom niżej, więc kwestia terminowania stringa (ograniczania spacją/NULLem) nie istnieje]
Mam nadzieję, że do tego miejsca jest wszystko jasne. Jeśli nie, to przeczytaj ponownie co napisałem powyżej – musisz to rozumieć, aby zrozumieć to, co przeczytasz poniżej.
– "Wiek" bita, czyli kolejności
Skoro wiemy już, że byte (B) przechowuje 8 bitów, spróbujmy zapisać w nim jakieś wartości liczbowe. Dla przykładu, niech będzie to 78, czyli:
01001110
Wspaniale. Proste, prawda? Jeśli nie, program Windows Kalkulator może przemieniać te wartości (w wersji zaawansowanej/profesjonalnej). Faktycznie – 1001110 = 78. Przy okazji – to nieistotne ile zer przed tą wartością wpiszę – nie zmieni się, ale przyjęło się aby 'dopełniać' wartości do ilości bitów – tu dopełniłem do 8śmiu pozycji (byte = 8bit).
Z tego miejsca łatwo policzyć, ile wartości maksymalnie pamiętać może 8 bitów – jeśli 'zapalimy' (wstawimy 1) każdy bit, uzyskamy 11111111, które – kalkulator nam powie ;) – jest równe 255. Na 1 bajcie (1B) możemy więc zapisać 256 różnych wartości – od 0 do 255.
Łatwo w tym momencie zauważyć, że od prawej do lewej wartości 'pozycji' rosną (1 = 1, 10 = 2, 100 = 4, 1000 = 8 – i tak dalej). Jest to intuicyjne (oczywiście, tak samo jest w systemie dziesiętnym). Dla ułatwienia rozmów na ten temat wprowadzono coś takiego jak 'wysokość' bita (w jęz. angielskim), lub jego 'wiek' (w jęz. polskim). Zasada jest prosta – im bardziej na lewo, tym 'starszy' ('higher') bit, a im bardziej na prawo – tym 'mlodszy' ('lower'). Zasada ta tyczy się wszystkich określeń tego typu (!).
Bit najbardziej na lewo jest 'najstarszy', a najbardziej na prawo – 'najmłodszy'. Aby to sobie łatwo zapamiętać: im 'starszy' bit, tym większą wartością 'zarządza'.
– Znak wartości
No dobrze... ale co się stanie, jeśli chcę zapisać np. –12? No cóż... sprawy się nieco komplikują. Wprowadza się wartość typu 'signed' (np. 'signed byte'). Co to oznacza? Ano tyle, że
umownie przyjmuje się, iż najstarszy bit (najbardziej na lewo, pamiętasz?) sygnalizować będzie znak 'minus'. Oznacza to 'przesunięcie skali' dla zmiennej. Wciąż potrafi przyjąć 256 różnych wartości, ale teraz – także ujemnych – więc największa i najmniejsza wartość się zmieniają. Przykładowe –12 zapiszę tak:
10001100
czyli:
(1) (0001100)
(–) (8+4) = –12
Dla kontrastu, +12 = 00001100.
(wspomnę tutaj, iż można na temat bitu mówić, że jest 'zapalony' (1) lub 'zgaszony' (0))
Tyle, że... co z wartościami?
unsigned byte (nieoznakowany bajt)
11111111 = 255
01111111 = 127
00000000 = 0
signed byte (oznakowany bajt)
11111111 = –127
01111111 = 127
00000000 = 0
"No dobrze. Ale ... to tylko 255 różnych wartości! Co się stało z tą 256–stą?" Spokojnie, bez nerwów. Popatrz:
00000000 = +0
10000000 = –0
...oto, co się stało.
Swoją drogą – najstarszy bit w tym przykładzie nie pełni funkcji ściśle liczbowej, tj. nie reprezentuje wartości, tylko pewną własność pamiętanej liczby. Takie wartości często określa się mianem
flag, gdyż 'sygnalizują' obecność jakiejś cechy. Oczywiście, flagi można zrobić właściwie 'ze wszystkiego'. Ale o tym kiedy indziej.
– System heksadecymalny (szesnastkowy)
Zapisywanie wartości bitowo jest ... niewygodne, ujmując delikatnie. Zapisywanie wartości dziesiętnie też do najwygodniejszych nie należy – bardzo duże wartości potrafią wciąż zająć bardzo dużo miejsca (wizualnie). Zresztą, sam do końca nie wiem, po kiego grzyba wymyślono system heksadecymalny (hex), ale przyznać muszę, że jest bardzo przydatny. Zachodzi przy tym pewna prawidłowość, która się przydaje najbardziej ze wszystkich.
System heksadecymalny ma u podstawy liczbę 16. Liczy się tak samo, np. 125 jest równe 112 + 13. Ale idźmy o krok dalej – zapiszmy tę wartość jako dwa znaki:
7D
Dlaczego tak? Ano, 10 jest już dwuznakowe, a potrzebujemy jeden. W związku z tym:
10 = A
11 = B
12 = C
13 = D
14 = E
15 = F
W efekcie nasze '7D' czytamy znów od końca (od prawej do lewej):
D(13)* 16^0 + 7* 16^1 = 13*1 + 7*16 = 13 + 112 = 125
Zachodzi tutaj bardzo ciekawa prawidłowość (o której istnieniu już wspomniałem) – jedna pozycja heksadecymalna odpowiada 4 bitom. W związku z tym np. bajt (8bit) możemy zapisać za pomocą dwóch znaków, zachowując 'wrażenie wizualne', że mowa o wartości 'rozumianej przez komputer'.
Przeliczanie z binarnego na heksadecymalny staje się dzięki temu dziecinnie proste. Dla przykładu weźmy liczbę 116, która w systemie binarnym jest równa:
01110100
Mówiłem, że jedna pozycja hex jest równa 4 bitom. Podzielmy więc to na segmenty, i liczmy:
0111 0100
0111 = 7
0100 = 4
Wynik: 74 heksadecymalnie (kalkulator nam powie, że mamy rację). Proste, prawda?
A teraz bardzo ważna konwencja: przyjęło się, aby liczby heksadecymalne zapisywać z dopełnieniem do ilości pozycji wielokrotności 2, oraz symbolem '0x' przed nimi. Na przykład liczba 12, która w systemie hex będzie mieć wartość C, powinna zostać zapisana jako 0x0C. Swoją drogą – 0x0C opisuje BAJT (8 bitów = 2 pozycje hex po 4 bity).
W ten sam sposób warto zapisywać inne 'pojemności' typów, np. 0x0C to to samo co 0x000C, ale pierwsze opisuje BAJT, a drugie – INT. Mam głęboką nadzieję, że wydaje się to sensowne i zrozumiałe.
– Kolejność bajtów i bitów w transmisji danych
Kiedy przychodzi czas aby zacząć pisać programy używające sieci, najczęstszy błąd to zapominanie, że istnieje wiele różnych architektur systemów. Różne procesory, na przykład, zapamiętują wartości w różnych kolejnościach – i ta własność jest też wyraźna w komunikacji sieciowej. Szczególnie telefony komórkowe i PDA/palmtopy są znane z takiej różnorodności (co nowy telefon, to nowy procesor w nim ;P).
Dla ułatwienia sprawy wprowadzono więc pewne pojęcia określające pozycje całych bajtów czy nibbli w transmisji danych:
Kolejność bajtów (8bit):
- Big–Endian – wartości są przesyłane 'tak jak widać', czyli np. jeśli na swoim komputerze wyślę 0x12 0x34 0x56, na komputerze z drugiej strony uzyskam 0x12 0x34 0x56
- Little–Endian – wartości są przesyłane w kolejności ODWROTNEJ, czyli jeśli na swoim komputerze wyślę 0x12 0x34 0x56, na komputerze z drugiej strony uzyskam 0x56 0x34 0x12
Kolejność nibbli (4bit):
- High–nibble first (najpierw wysoki) – wartości są przesyłane 'tak jak widać', czyli np. jeśli na swoim komputerze wyślę 0x12, na komputerze z drugiej strony uzyskam 0x12
- Low–nibble first (najpierw niski) – wartości są przesyłane w kolejności ODWROTNEJ, czyli jeśli na swoim komputerze wyślę 0x12, na komputerze z drugiej strony uzyskam 0x21
Podsumuję na przykładzie:
Wysyłam wartość 0x123456 z komputera A do komputera B. Komputer A stosuje format Big–Endian high–nibble first, komputer B – Little–Endian low–nibble first. Po przesłaniu danych, wartości są następujące:
Komputer A: 0x12 0x34 0x56
Komputer B: 0x65 0x43 0x21
To, który komputer musiał przeprowadzić konwersję danych do 'swojego' formatu jest kwestią umowną. Programiści mogą umówić się, że wszystkie dane przesyłane między tymi komputerami będą miały postać "Big–Endian low–nibble first", i programy po obu stronach kabla są odpowiedzialne za przeprowadzenie właściwych operacji na wychodzących i przychodzących danych.
Zbiór takich umów między programistami nazywa się
protokołem komunikacji.
Na tym zakończę pierwszą część z cyklu artykułów traktujących o programowaniu. W drugiej części powiem co–nieco o tym, co to dokładnie jest protokół komunikacji, i jak korzystać z wiedzy o protokole.