Rozmiary obiektów
Jak nauczyłeś się na lekcji 4.1 -- Wprowadzenie do podstawowych typów danych, pamięć nowoczesnych maszyn jest zazwyczaj zorganizowana w jednostki wielkości bajtów, przy czym każdy bajt pamięci ma unikalny adres. Do tego momentu przydatne było myślenie o pamięci jako o zbiorze schowków lub skrzynek pocztowych, w których możemy umieszczać i pobierać informacje, a o zmiennych jako o nazwach umożliwiających dostęp do tych schowków lub skrzynek pocztowych.
Jednak ta analogia nie jest do końca poprawna pod jednym względem - większość obiektów w rzeczywistości zajmuje więcej niż 1 bajt pamięci. Pojedynczy obiekt może używać 1, 2, 4, 8 lub nawet większej liczby kolejnych adresów pamięci. Ilość pamięci wykorzystywanej przez obiekt zależy od jego typu danych.
Ponieważ dostęp do pamięci zazwyczaj uzyskujemy poprzez nazwy zmiennych (a nie bezpośrednio poprzez adresy pamięci), kompilator jest w stanie ukryć przed nami szczegółowe informacje o tym, ile bajtów wykorzystuje dany obiekt. Kiedy uzyskujemy dostęp do jakiejś zmiennej x w naszym kodzie źródłowym, kompilator wie, ile bajtów danych należy pobrać (w oparciu o typ zmiennej x) i wygeneruje odpowiedni kod języka maszynowego, aby obsłużyć te szczegóły za nas.
Mimo to istnieje kilka powodów, dla których warto wiedzieć, ile pamięci wykorzystuje obiekt.
Po pierwsze, im więcej pamięci wykorzystuje obiekt, tym więcej informacji może uzyskać wstrzymaj.
Pojedynczy bit może pomieścić 2 możliwe wartości, 0 lub 1:
| bit 0 |
|---|
| 0 |
| 1 |
2 bity mogą pomieścić 4 możliwe wartości:
| bit 0 | bit 1 |
|---|---|
| 0 | 0 |
| 0 | 1 |
| 1 | 0 |
| 1 | 1 |
3 bity mogą pomieścić 8 możliwych wartości:
| bit 0 | bit 1 | bit 2 |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 0 | 1 |
| 0 | 1 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 0 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
| 1 | 1 | 1 |
Uogólniając, obiekt z n bitami (gdzie n jest liczbą całkowitą) może przechowywać 2n (2 do potęgi n, również powszechnie zapisywanej jako 2^n) unikalnych wartości. Dlatego przy 8-bitowym bajcie obiekt wielkości bajtu może przechowywać 28 (256) różnych wartości. Obiekt korzystający z 2 bajtów może przechowywać 2^16 (65536) różnych wartości!
W związku z tym rozmiar obiektu ogranicza ilość unikalnych wartości, które może przechowywać — obiekty wykorzystujące więcej bajtów mogą przechowywać większą liczbę unikalnych wartości. Zbadamy to bliżej, gdy będziemy mówić więcej o liczbach całkowitych.
Po drugie, komputery mają skończoną ilość wolnej pamięci. Za każdym razem, gdy definiujemy obiekt, niewielka część tej wolnej pamięci jest używana tak długo, jak obiekt istnieje. Ponieważ nowoczesne komputery mają dużo pamięci, wpływ ten jest zwykle znikomy. Jednakże w przypadku programów, które wymagają dużej ilości obiektów lub danych (np. gra renderująca miliony wielokątów) różnica pomiędzy wykorzystaniem obiektów 1-bajtowych i 8-bajtowych może być znacząca.
Kluczowa informacja
Nowi programiści często zbytnio skupiają się na optymalizacji swojego kodu, aby zużywać jak najmniej pamięci. W większości przypadków robi to pomijalną różnicę. Skoncentruj się na pisaniu łatwego w utrzymaniu kodu i optymalizuj tylko wtedy i tam, gdzie korzyści będą znaczące.
Podstawowe rozmiary typów danych
Kolejnym oczywistym pytaniem jest „ile pamięci zużywają obiekty danego typu danych?”. Być może zaskakujące jest to, że standard C++ nie definiuje dokładnego rozmiaru (w bitach) żadnego z typów podstawowych.
Zamiast tego standard mówi, co następuje:
- Obiekt musi zajmować co najmniej 1 bajt (aby każdy obiekt miał odrębny adres pamięci).
- Bajt musi mieć co najmniej 8 bitów.
- Typy całkowe
char,short,int,long, Ilong longmają minimalny rozmiar wynosi odpowiednio 8, 16, 16, 32 i 64 bity. charichar8_tto dokładnie 1 bajt (co najmniej 8 bitów).
Nomenklatura
Kiedy mówimy o rozmiarze typu, tak naprawdę mamy na myśli rozmiar tworzonego obiektu tego typu.
W tej serii tutoriali przedstawimy uproszczony widok, przyjmując pewne rozsądne założenia, które są ogólnie prawdziwe dla współczesnych architektury:
- Bajt to 8 bitów.
- Pamięć jest adresowalna bajtowo (możemy uzyskać dostęp do każdego bajtu pamięci niezależnie).
- Obsługa zmiennoprzecinkowa jest zgodna z IEEE-754.
- Jesteśmy w architekturze 32-bitowej lub 64-bitowej.
Biorąc pod uwagę powyższe założenia, możemy rozsądnie stwierdzić, co następuje:
| Kategoria | Typ | Minimum Rozmiar | Typowy rozmiar |
|---|---|---|---|
| Boolean | bool | 1 bajt | 1 bajt |
| Znak | char | 1 bajt (dokładnie) | 1 bajt |
| wchar_t | 1 bajt | 2 lub 4 bajty | |
| char8_t | 1 bajt | 1 bajt | |
| char16_t | 2 bajty | 2 bajty | |
| char32_t | 4 bajty | 4 bajty | |
| Całka | krótki | 2 bajty | 2 bajty |
| int | 2 bajty | 4 bajty | |
| long | 4 bajty | 4 lub 8 bajtów | |
| long long | 8 bajtów | 8 bajtów | |
| Zmiennoprzecinkowe | float | 4 bajty | 4 bajty |
| double | 8 bajtów | 8 bajtów | |
| long double | 8 bajtów | 8, 12 lub 16 bajty | |
| Wskaźnik | std::nullptr_t | 4 bajty | 4 lub 8 bajtów |
Wskazówka
Aby zapewnić maksymalną przenośność, nie powinieneś zakładać, że obiekty są większe niż określony rozmiar minimalny.
Alternatywnie, jeśli chcesz założyć, że typ ma rozmiar inny niż minimalny (np. że int ma wartość najmniej 4 bajtów), możesz użyć static_assert , aby kompilator nie wykonał kompilacji, jeśli jest skompilowany na architekturze, w której to założenie nie jest prawdziwe. Jak to zrobić na lekcji 9.6 — Assert i static_assert.
Powiązana treść
Możesz znaleźć więcej informacji o tym, co standard C++ mówi o minimalnym rozmiarze różnych typów tutaj.
Operator sizeof
Aby określić rozmiar typów danych na konkretnej maszynie, C++ udostępnia operator o nazwie sizeof. Słowo kluczowe sizeofoperator jest operatorem jednoargumentowym, który przyjmuje typ lub zmienną i zwraca rozmiar obiektu tego typu (w bajtach). Możesz skompilować i uruchomić następujący program, aby sprawdzić, jak duże są niektóre typy danych:
#include <iomanip> // for std::setw (which sets the width of the subsequent output)
#include <iostream>
#include <climits> // for CHAR_BIT
int main()
{
std::cout << "A byte is " << CHAR_BIT << " bits\n\n";
std::cout << std::left; // left justify output
std::cout << std::setw(16) << "bool:" << sizeof(bool) << " bytes\n";
std::cout << std::setw(16) << "char:" << sizeof(char) << " bytes\n";
std::cout << std::setw(16) << "short:" << sizeof(short) << " bytes\n";
std::cout << std::setw(16) << "int:" << sizeof(int) << " bytes\n";
std::cout << std::setw(16) << "long:" << sizeof(long) << " bytes\n";
std::cout << std::setw(16) << "long long:" << sizeof(long long) << " bytes\n";
std::cout << std::setw(16) << "float:" << sizeof(float) << " bytes\n";
std::cout << std::setw(16) << "double:" << sizeof(double) << " bytes\n";
std::cout << std::setw(16) << "long double:" << sizeof(long double) << " bytes\n";
return 0;
}Oto dane wyjściowe z komputera autora:
bool: 1 bytes char: 1 bytes short: 2 bytes int: 4 bytes long: 4 bytes long long: 8 bytes float: 4 bytes double: 8 bytes long double: 8 bytes
Twoje wyniki mogą się różnić w zależności od kompilatora, architektury komputera, systemu operacyjnego, ustawień kompilacji (32-bitowa vs 64-bitowa) itp...
Próba użycia sizeof na niekompletnym typie (takim as void) spowoduje błąd kompilacji.
Dla użytkowników gcc
Jeśli nie wyłączyłeś rozszerzeń kompilatora, gcc pozwala sizeof(void) zwrócić 1 zamiast generować diagnostykę (Pointer-Arith). Jak wyłączyć rozszerzenia kompilatora pokazujemy w lekcji 0.10 -- Konfigurowanie kompilatora: Rozszerzenia kompilatora.
Możesz także użyć operatora sizeof na nazwie zmiennej:
#include <iostream>
int main()
{
int x{};
std::cout << "x is " << sizeof(x) << " bytes\n";
return 0;
}x is 4 bytes
Dla zaawansowanych czytelników
sizeof nie obejmuje dynamicznie alokowanej pamięci używanej przez obiekt. Dynamiczną alokację pamięci omówimy w przyszłej lekcji.
Podstawowa wydajność typów danych
Na nowoczesnych maszynach obiekty podstawowych typów danych są szybkie, więc wydajność podczas używania lub kopiowania tych typów zasadniczo nie powinna stanowić problemu.
Na marginesie…
Można założyć, że typy zużywające mniej pamięci będą szybsze niż typy wykorzystujące więcej pamięci. Nie zawsze jest to prawdą. Procesory są często optymalizowane do przetwarzania danych o określonym rozmiarze (np. 32 bity), a typy pasujące do tego rozmiaru mogą być przetwarzane szybciej. Na takiej maszynie 32-bitowy int może być szybszy niż 16-bitowy krótki lub 8-bitowy znak.

