System typów w C pojawił się już na początku języka (koniec lat 60. - początek 70. XX wieku). Ścisła statyczna typizacja pozwala kompilatorowi sprawdzać zgodność typów zmiennych, wyrażeń i wartości zwracanych przed etapem wykonania programu.
Historia zagadnienia:
Statyczna typizacja została wprowadzona, aby z wyprzedzeniem zapobiegać błędom, które mogłyby zostać odkryte tylko w trakcie wykonywania. Z czasem system typów w C stopniowo komplikował się w celu wsparcia nowych platform i stylów programowania.
Problem:
Błąd niezgodności typów może prowadzić do nieprzewidywalnych konsekwencji: uszkodzenia pamięci, błędnych obliczeń, awaryjnego zakończenia programu. Bez statycznego sprawdzania takich sytuacji trudno uniknąć.
Rozwiązanie:
Kod w C sprawdza typy zmiennych i wyrażeń na etapie kompilacji. Na przykład, nie można przypisać wskaźnika na int zmiennej typu float* bez jawnego rzutowania typu. To zapobiega wielu błędom.
Przykład kodu:
int x = 5; double y = 3.14; y = x; // niejawne rozszerzenie typu int -> double int* p = &x; double* q = (double*)p; // dozwolone, ale niebezpieczne!
Kluczowe cechy:
Dlaczego w C można "rzucić" każdy wskaźnik na void i z powrotem bez utraty informacji?*
Standard C gwarantuje, że wskaźnik dowolnego typu może być rzucony na void* i z powrotem bez utraty informacji. To jest używane na przykład w funkcjach biblioteki standardowej (malloc, memcpy). Jednak rzutowanie void* z powrotem na niewłaściwy typ prowadzi do nieokreślonego zachowania.
Jak zachodzi niejawna konwersja typów przy operacjach arytmetycznych między int a float?
C automatycznie "przesuwa" mniejszy typ do większego, zwykle na double lub float. Na przykład, jeśli dodawane są int i float, to int jest konwertowany na float przed operacją.
int a = 10; float b = 2.5f; float c = a + b; // a najpierw jest konwertowane na float
Czy to prawda, że wskaźnik na void nie może być dereferencjonowany?
Tak, wskaźnik na void wskazuje na dane nieokreślonego typu i nie może być dereferencjonowany bezpośrednio, ponieważ kompilator nie zna rozmiaru typu. Aby zrealizować dereferencję, musisz rzutować na konkretny typ:
void* ptr = ...; int x = *(int*)ptr;
Przekazywanie wskaźników różnych typów do funkcji przyjmującej void*, bez późniejszego odpowiedniego rzutowania:
void print_value(void* data) { printf("%d ", *(int*)data); // błąd, jeśli data — to double* } double d = 1.5; print_value(&d); // niepoprawne
Plusy:
Minusy:
Użycie statycznej typizacji i jawnych konwersji z weryfikacją:
void print_int(void* data) { if (data) { printf("%d ", *(int*)data); } } int value = 42; print_int(&value);
Plusy:
Minusy: