Jasne jest chyba to, że dane pewnego rodzaju zajmują pewien obszar pamięci. Jasne z pewnością jest również to, że dane dające większy zakres albo większą dokładność obliczeń muszą zajmować tej pamięci więcej niż dane dające mniejszy zakres lub mniejszą dokładność. Jak zauważyłeś, stałe można zaklasyfikować do różnych rodzajów - także i typy można bardzo podobnie zaklasyfikować.
Jako pierwsze wymienię tu typy całkowite, bo jest ich najwięcej. Tu wymienione są nazwy, typowa zajętość pamięci i zakres liczb danych tych typów.
| Nazwa | Zajętość (typowo) | Zakres |
|---|---|---|
| char | 1 bajt | |
| signed char | 1 bajt | |
| unsigned char | 1 bajt | |
| short int | 2 bajty | |
| int | 2 lub 4 bajty | |
| unsigned int | 2 lub 4 bajty | |
| long int | przynajmniej 4 bajty | |
| unsigned long int | przynajmniej 4 bajty |
Ilość zajmowanej pamięci w przypadku niektórych typów nie jest stała i zależy od konkretnego kompilatora. Muszę tu wspomnieć o standardzie ANSI (American National Standard Institute), który stawia pewne wymagania przed różnymi kompilatorami. Służy to zapewnieniu przenośności na poziomie kodu źródłowego, tzn. programista piszący program na kompilatorze stosującym wymogi ANSI i sam trzymający się tych wymogów ma zagwarantowane to, że jego program powinien dać się skompilować (i uruchomić) na innym kompilatorze zgodnym ze standardem ANSI. Wytyczne ANSI dla programisty odnośnie różnych typów danych na różnych kompilatorach nakazują mu, aby swojego programu nigdy nie opierał na ilości pamięci zajmowanej przez te typy. Przykład: poprawnie napisany program powinien dobrze działać spod kompilatora, w którym typ int ma 2 bajty, jak też tam gdzie typ ten ma 4 bajty (np. w 32-bitowym środowisku Windows). Jeśli zaś koniecznie trzeba oprzeć się na konkretnej ilości pamięci w swoim programie, wówczas należy przeczytać dokumentację (np. opis typów danych) dla używanego kompilatora i pamiętać, że program ten będzie poprawnie kompilowany tylko na niektórych (nie wszystkich) kompilatorach.
Następna rzecz to taka, że słowa unsigned i signed oraz short i long (w odniesieniu do typów int) są swego rodzaju modyfikatorami i w zależności od kontekstu mogą zostać pominięte, jak też samo int w ich kontekście może zostać pominięte.
Oto przykłady:
short int = short
int = signed lub signed int
unsigned int = unsigned
long int = long lub signed long lub signed long int
unsigned long int = unsigned long
Jednak przy typach char nie ma już takiej dowolności, bo to czy typ znakowy jest signed czy unsigned, zależy od kompilatora, zatem, według zaleceń ANSII, chcąc użyć tego typu jako liczb jednobajtowych bez znaku, lepiej jest podać unsigned char.
Przekroczenie zakresu liczb wszystkich typów całkowitych powoduje "zawinięcie się" wartości, np. dla kompilatora, w którym typ int jest 2-bajtowy:
"Na szczęście" typów rzeczywistych jest mniej - tylko trzy. Tutaj podam nazwę, zajętość pamięci, zakres i dokładność liczb danych typów rzeczywistych.
| Nazwa | Zajętość | Zakres | Cyfry znaczące |
|---|---|---|---|
| float | 32 bity | 7 do 8 | |
| double | 64 bity | 15 do 16 | |
| long double | 80 bitów | 19 do 20 |
W kolumnie "Cyfry znaczące" powyższej tabeli napisałem "od-do", bo ostatnia cyfra liczb rzeczywistych często nie jest wystarczająco dokładna. Jeśli więc, dla przykładu, potrzebna jest Ci dobra dokładność aż do 8-mej cyfry po kropce, to lepiej użyć typu double niż float. Ilość cyfr znaczących oznacza też wielkość utraty dokładności dla dużych liczb. Na przykład po dodaniu 1.0F do 3.1e20F (stałe typu float) otrzymamy dziwny wynik 3.1e20F. Przestaje on być dziwny, gdy uświadomisz sobie, że 2.0e+20F, to liczba dwudziestojedencyfrowa (!), a z racji tego, że dana jest typu float, z liczby tej "pamiętane" jest tylko osiem pierwszych cyfr.
Co można jeszcze powiedzieć o liczbach rzeczywistych? To, że po przekroczeniu zakresu wzwyż, otrzymuje się komunikat błędu (Overflow error), a po przekroczeniu go poniżej jego dolnej granicy liczba staje się zerem. Dzielenie przez taką liczbę także będzie niemiłą niespodzianką (Division by zero error).
Wyżej wymienione typy danych, to typy podstawowe. Wszystkie pozostałe są typami pochodnymi, bo są zbudowane na bazie tych podstawowych. Najważniejsze są tu typy wskaźnikowe. Wskaźnik (ang. pointer), to po prostu adres miejsca w pamięci, gdzie przechowywana jest dana pewnego typu. Jak się później okaże, może to być wskaźnik na dowolny obiekt (również wskaźnik na jakiś nieokreślony obiekt). Prawie zawsze jednak razem ze wskaźnikiem skojarzony jest typ danej (być może klasa obiektów) przezeń wskazywanej. W deklaracjach zamiar użycia wskaźnika określa się pisząc w odpowiednim miejscu znak * (gwiazdka). W najprostszych przypadkach (bo będą, oj będą bardziej skomplikowane) może to wyglądać tak jak w poniższych przykładach:
int * - wskaźnik na daną całkowitą;
unsigned long * - wskaźnik na daną całkowitą bez znaku;
double * - wskaźnik na daną rzeczywistą podwójnej precyzji.
A tu jest wskaźnik nie skojarzony z żadnym typem danej, po prostu wskazujący na nieokreślony obszar pamięci:
void *
Chociaż każdy wskaźnik (choćby miał jakąś nonsensowną wartość) wskazuje na jakieś miejsce pamięci, to twórcy języka C (i C++) wprowadzili umowę, że gdy wskaźnik ma wartość NULL (nie mylić ze znakiem NULL), to nie wskazuje na nic. Co to jest te NULL? W większości kompilatorów jest to po prostu stała kompilatora zdefiniowana następująco:
#define NULL 0
albo
#define NULL 0L
Czyli, gdy wskaźnik zawiera liczbę zero, to nie wskazuje na nic. Ale uwaga: standard ANSI nakazuje, by nie używać bezpośrednio liczby 0, tylko jednak stałej NULL, bo nie we wszystkich kompilatorach stała ta jest zdefiniowana jak powyżej.
Odłóżmy na chwilę wskaźniki, wrócimy do nich przy omawianiu deklaracji i definicji. Tutaj chciałem tylko je przedstawić i wytłumaczyć do czego służą.
To co tu opiszę, nazywa się typy wyliczeniowe (ang. enumeration) i jest to pewien podzbiór typów całkowitych, który sami definiujemy. Nie było ich we wcześniejszych wersjach kompilatorów C. Dopiero dalsze wersje, a także kompilatory języka C++ w nie wyposażono. Na następnej podstronie są zawarte uwagi dotyczące notacji używanej przeze mnie do objaśniania kodu. Deklaracja stałych wyliczeniowych (i, być może całego typu) wygląda następująco:
enum [ <nazwa_typu> ] { <nazwa_stałej> [ = <stała_całkowita> ] , ... <nazwa_stałej> [ = <stała_całkowita> ] };Na przykład:
enum kolory
{
czarny = 1,
biały,
czerwony,
niebieski,
zielony,
żółty
};
Zdefiniowany tu został typ wyliczeniowy o nazwie kolory. Wylicza on kolejne stałe i nadaje im realne wartości całkowite. Stała czarny ma nadaną wartość 1, a kolejne stałe mają wartości kolejno wyliczane, tzn. o jeden większe od swoich poprzedników; np. stała żółty ma więc wartość 6. Gdyby przy wyliczeniu stałej czarny nie było znaku = (równe) i cyfry 1 (czyli nadania wartości 1), to wyliczanie rozpoczęłoby się od zera. Nadawanie wartości może być użyte w dowolnym momencie - w szczególności wszystkie stałe mogą mieć wartości nadane.
Po deklaracji typu wyliczeniowego stałe o deklarowanych nazwach funkcjonują tak samo, jak stałe całkowite, a typ o deklarowanej nazwie - jak typ całkowity. Standard ANSI mówi, że to czy kompilator traktuje stałe wyliczane identycznie jak stałe całkowite, czy nie, zależy od samego kompilatora i nie należy na tym polegać. Dla powyższego przykładu: lepiej jest deklarować dane typu kolory niż typu int, kiedy zachodzi potrzeba posłużenia się którąś ze stałych wyliczanych (czarny, biały, czerwony, niebieski, zielony lub żółty).
Na koniec warto powiedzieć nieco o tablicach, strukturach, uniach i klasach. Wrócimy do nich przy omawaniu deklaracji. Tablice, struktury i unie, to tzw. agregaty, czyli obiekty grupujące w sobie więcej niż jedną daną. Występowały one już we wcześniejszych językach programowania (na przykład w Pascal'u struktury nazywane są rekordami, ale funkcjonują tak samo). Klasy można traktować podobnie jak struktury, jako pewnego rodzaju typ danych, definiowany przez samego programistę, który może mieć własne dane (tzw. pola) i funkcje składowe (tzw. metody); dostęp do nich może być dowolnie definiowany; można też definiować operacje możliwe do wykonania na obiektach takich klas.