Dlaczego typy mają znaczenie w C++
Gdy deklarujesz wartość z jawnym typem, na przykład int age = 30;, typ to nie tylko etykieta. Mówi kompilatorowi, ile bajtów zarezerwować, jak te bajty interpretować i jakie operacje są dozwolone. Pomyl typ, a możesz po cichu stracić precyzję, doprowadzić do przepełnienia albo wywołać niezdefiniowane zachowanie.
C++ dzieli wbudowane typy na kilka rodzin: liczby całkowite, liczby zmiennoprzecinkowe, typ znakowy i typ logiczny. Przyjrzyjmy się każdej z nich, a potem regułom, na których ludzie się potykają.
Typy podstawowe
Oto po jednym z każdego podstawowego typu w jednym programie. Zwróć uwagę na przyrostki literałów (L, f, u) i pojedyncze cudzysłowy przy char:
bool domyślnie wypisuje się jako 1 albo 0, a nie true/false. char używa pojedynczych cudzysłowów: 'A' to jeden znak, a "A" (podwójne cudzysłowy) to literał napisowy, zupełnie inny typ. Te dwie pomyłki są na początku niezwykle częste.
Rozmiary nie są stałe
To największe zaskoczenie dla osób przychodzących z języków takich jak Java. Standard C++ gwarantuje tylko minimalne rozmiary i względną kolejność (short ≤ int ≤ long ≤ long long). Faktyczny rozmiar zależy od kompilatora i platformy. Zawsze sprawdzaj go przez sizeof:
W typowej 64-bitowej kompilacji na Linuksie zobaczysz int = 4, long = 8. Na 64-bitowym Windowsie long ma jednak tylko 4 bajty. Ta luka w przenośności to dokładnie powód, dla którego nie należy pisać kodu zakładającego, że long jest 64-bitowy.
Gdy potrzebujesz dokładnej szerokości, sięgnij po typy całkowite o stałej szerokości z <cstdint>:
Używaj int32_t/int64_t w formatach plików, protokołach sieciowych i wszędzie tam, gdzie zachowanie musi być identyczne na różnych maszynach. Zwróć uwagę na rzutowanie (int)a: wysłanie 8-bitowego typu do strumienia wypisuje go jako znak, a nie liczbę, więc najpierw go rzutuj.
signed kontra unsigned
Każdy typ całkowity występuje w dwóch odmianach. Typ signed może przechowywać liczby ujemne; typ unsigned nie może, ale w zamian za zakres ujemny ma wyższe maksimum dodatnie. Zwykły int jest domyślnie ze znakiem.
Odejmowanie od 0 bez znaku zawija się do ogromnej liczby dodatniej zamiast zejść poniżej zera. To ciągle gryzie ludzi, zwłaszcza przy size_t (typie bez znaku) zwracanym przez .size():
vector<int> v = {1, 2, 3};
// UWAGA: v.size() jest bez znaku. Jeśli v jest pusty, v.size() - 1 zawija się
// do gigantycznej liczby, a pętla kręci się niemal w nieskończoność.
for (size_t i = 0; i <= v.size() - 1; i++) { /* ... */ }
Wybieraj i < v.size() (nigdy <= size() - 1) albo całkiem omiń problem dzięki pętli for po zakresie.
Przepełnienie liczb całkowitych to niezdefiniowane zachowanie
W przeciwieństwie do zawijania liczb bez znaku (które jest dobrze zdefiniowane), przepełnienie liczb całkowitych ze znakiem to w C++ niezdefiniowane zachowanie. Kompilator może zrobić cokolwiek: zwrócić śmieci, wyoptymalizować sprawdzenie albo doprowadzić do awarii:
Rozwiązanie jest takie samo jak przy pułapce przepełnienia w każdym języku: wykonuj obliczenia w szerszym typie. Rzutuj jeden z operandów na long long przed +, aby dodawanie odbyło się na 64 bitach. Rzutowanie wyniku po fakcie jest spóźnione, bo przepełnienie już nastąpiło.
Wybór właściwego typu
W większości kodu wartości domyślne są w porządku: int dla liczb całkowitych, double dla ułamków. Po coś innego sięgaj tylko wtedy, gdy masz powód.
| Typ | Typowy rozmiar | Kiedy używać |
|---|---|---|
int | 32 bity | Domyślny dla liczb całkowitych |
long long | 64 bity | Wartości powyżej ok. 2 miliardów: znaczniki czasu, duże liczniki |
double | 64 bity | Domyślny dla ułamków, dobra precyzja |
float | 32 bity | Tablice, w których liczy się pamięć, a precyzja może ucierpieć |
bool | 1 bajt | Flaga prawda/fałsz |
int32_t / int64_t | dokładny | Formaty międzyplatformowe, protokoły, operacje na bitach |
Kilka pułapek do zapamiętania. float ma tylko około 7 znaczących cyfr dziesiętnych, więc 0.1f + 0.2f to nie dokładnie 0.3; wybieraj double, chyba że naprawdę musisz oszczędzać pamięć. A char może być ze znakiem lub bez, zależnie od platformy, więc jeśli wykonujesz obliczenia na surowych bajtach, pisz jawnie signed char albo unsigned char.
Dalej: słowo kluczowe auto
Wypisywanie typu za każdym razem bywa żmudne, a czasem typ jest długi albo trudny do nazwania. C++ pozwala kompilatorowi wywnioskować go za ciebie dzięki słowu kluczowemu auto: auto x = 42; sprawia, że x jest typu int, a auto it = v.begin(); oszczędza ci wpisywania rozwlekłego typu iteratora. Następna strona pokazuje, kiedy auto czyni kod czytelniejszym, a kiedy ukrywa za dużo.
Najczęściej zadawane pytania
Jakie są podstawowe typy danych w C++?
Typy podstawowe to liczby całkowite (short, int, long, long long), zmiennoprzecinkowe (float, double, long double), typ znakowy char i typ logiczny bool. Każdy typ całkowity może też być signed lub unsigned. Wszystko inne, czyli std::string, tablice i twoje własne klasy, jest zbudowane na ich bazie.
Jaka jest różnica między int a long w C++?
Oba przechowują liczby całkowite, ale long ma gwarancję, że jest co najmniej tak szeroki jak int (często 64-bitowy na platformach 64-bitowych, ale na Windowsie tylko 32-bitowy). Standard ustala jedynie minimalne rozmiary, więc gdy potrzebujesz gwarantowanej szerokości, użyj typów o stałej szerokości z <cstdint>, takich jak int32_t i int64_t.
Ile zajmuje int w C++?
Standard gwarantuje tylko, że int ma co najmniej 16 bitów, ale praktycznie na każdym nowoczesnym komputerze i serwerze ma 32 bity. Rozmiary zależą od platformy, więc nigdy ich nie zakładaj: wypisz sizeof(int), aby sprawdzić, albo użyj typów z <cstdint>, takich jak int32_t, gdy potrzebujesz dokładnej szerokości.