4.6 — Liczby całkowite o stałej szerokości i rozmiar_t

W poprzednich lekcjach na temat liczb całkowitych omawialiśmy, że C++ gwarantuje jedynie, że zmienne całkowite będą miały minimalny rozmiar - ale mogą być większe, w zależności od systemu docelowego.

Na przykład an int ma minimalny rozmiar 16 bitów, ale w nowoczesnych architekturach jest to zazwyczaj 32 bity.

Jeśli założysz an int jest 32-bitowy, ponieważ jest to najbardziej prawdopodobne, wówczas Twój program prawdopodobnie będzie działał nieprawidłowo na architekturach, w których int jest w rzeczywistości 16-bitowy (ponieważ prawdopodobnie będziesz przechowywać wartości wymagające 32-bitowej pamięci w zmiennej zawierającej tylko 16-bitową pamięć, co spowoduje przepełnienie lub niezdefiniowane zachowanie).

Na przykład:

#include <iostream>

int main()
{
    int x { 32767 };        // x may be 16-bits or 32-bits
    x = x + 1;              // 32768 overflows if int is 16-bits, okay if int is 32-bits
    std::cout << x << '\n'; // what will this print?

    return 0;
}

Na komputerze gdzie int jest 32-bitowy, wartość 32768 mieści się w zakresie int i dlatego może być przechowywana w x bez problemu. Na takiej maszynie ten program wydrukuje 32768. Jednakże na maszynie, gdzie int jest 16-bitowa, wartość 32768 nie mieści się w zakresie 16-bitowej liczby całkowitej (która ma zakres od -32 768 do 32 767). Na takiej maszynie x = x + 1 spowoduje przepełnienie, a wartość -32768 zostanie zapisana w x a następnie wydrukowana.

Zamiast tego, jeśli założysz, że int ma tylko 16 bitów, aby zapewnić, że Twój program będzie zachowywał się na wszystkich architekturach, wówczas zakres wartości, które możesz bezpiecznie przechowywać w int jest znacznie ograniczony. A w systemach, w których int jest w rzeczywistości 32-bitowy, nie wykorzystuje się połowy pamięci przydzielonej na int.

Kluczowa informacja

W większości przypadków tworzymy instancję tylko niewielkiej liczby int zmiennych na raz i są one zazwyczaj niszczone na końcu funkcji, w której zostały utworzone. W takich przypadkach marnowanie 2 bajtów pamięci na zmienną nie stanowi problemu (ograniczony zakres jest większym problemem). Jednakże w przypadkach, gdy nasz program przydziela miliony int zmiennych, marnowanie 2 bajtów pamięci na zmienną może mieć znaczący wpływ na ogólne wykorzystanie pamięci przez program.

Dlaczego rozmiar typów całkowitych nie jest stały?

Krótka odpowiedź jest taka, że ​​sięga to początków C, kiedy komputery były wolne, a wydajność była najważniejsza. C zdecydował się celowo pozostawić rozmiar liczby całkowitej otwarty, aby osoby wdrażające kompilator mogły wybrać rozmiar dla int , który najlepiej sprawdza się w architekturze komputera docelowego. W ten sposób programiści mogliby po prostu używać int bez martwienia się, czy mogliby użyć czegoś bardziej wydajnego.

Według współczesnych standardów brak spójnych zakresów dla różnych typów całkowitych jest do niczego (szczególnie w języku zaprojektowanym z myślą o przenośności).

Liczby całkowite o stałej szerokości

Aby rozwiązać powyższe problemy, C++11 zapewnia alternatywny zestaw typów całkowitych, które gwarantują ten sam rozmiar w dowolnej architekturze. Ponieważ rozmiar tych liczb całkowitych jest stały, nazywane są one liczbami całkowitymi o stałej szerokości.

Liczby całkowite o stałej szerokości są zdefiniowane (w nagłówku <cstdint>\) w następujący sposób:

NazwaFixed SizeFixed Size ZakresUwagi
std::int8_t1 bajt ze znakiem-128 do 127Traktowane jak znak ze znakiem w wielu systemach. Zobacz uwagę poniżej.
std::uint8_t1 bajt bez znaku0 do 255Traktowane jak znak bez znaku w wielu systemach. Patrz uwaga poniżej.
std::int16_t2 bajty ze znakiem-32 768 do 32 767
std::uint16_t2 bajty bez znaku0 do 65 535
std::int32_t4 bajty ze znakiem-2 147 483 648 do 2 147 483 647
std::uint32_t4 bajty bez znaku0 do 4 294 967 295
std::int64_t8 bajtów ze znakiem-9 223 372 036 854 775 808 do 9 223 372 036 854 775 807
std::uint64_t8 bajtów bez znaku0 do 18 446 744 073 709 551 615

Oto przykład:

#include <cstdint> // for fixed-width integers
#include <iostream>

int main()
{
    std::int32_t x { 32767 }; // x is always a 32-bit integer
    x = x + 1;                // so 32768 will always fit
    std::cout << x << '\n';

    return 0;
}

Najlepsza praktyka

Użyj typu całkowitego o stałej szerokości, jeśli potrzebujesz typu całkowitego o gwarantowanym zakresie.

Ostrzeżenie: std::int8_t i std::uint8_t zwykle zachowuj się jak chars

Z powodu niedopatrzenia w specyfikacji C++, współczesne kompilatory zazwyczaj traktują std::int8_t i std::uint8_t (oraz odpowiadające im typy szybkie i typy o najmniejszej stałej szerokości, które za chwilę przedstawimy) odpowiednio tak samo jak signed char i unsigned char . Zatem w większości nowoczesnych systemów 8-bitowe typy całkowe o stałej szerokości będą zachowywać się jak typy znakowe.

W ramach krótkiej zapowiedzi:

#include <cstdint> // for fixed-width integers
#include <iostream>

int main()
{
    std::int8_t x { 65 };   // initialize 8-bit integral type with value 65
    std::cout << x << '\n'; // You're probably expecting this to print 65

    return 0;
}

Chociaż prawdopodobnie spodziewasz się, że powyższy program wydrukuje 65, najprawdopodobniej tak się nie stanie.

Omawiamy, co faktycznie drukuje ten przykład (i jak zapewnić, że zawsze drukuje 65) w lekcja 4.12 -- Wprowadzenie do konwersji typów i static_cast, po tym jak omówiliśmy znaki (i sposób ich drukowania) w lekcji 4.11 -- Znaki.

Ostrzeżenie

8-bitowe typy całkowite o stałej szerokości są często traktowane jak znaki, a nie wartości całkowite (co może się różnić w zależności od systemu). Ten problem nie dotyczy 16-bitowych i szerszych typów całkowitych.

Dla zaawansowanych czytelników

Liczby całkowite o stałej szerokości w rzeczywistości nie definiują nowych typów — są po prostu aliasami dla istniejących typów całkowitych o żądanym rozmiarze. Dla każdego typu o stałej szerokości implementacja (kompilator i biblioteka standardowa) określa, który istniejący typ jest aliasem. Przykładowo na platformie, gdzie int jest 32-bitowa, std::int32_t będzie aliasem dla int. W systemie, w którym int jest 16-bitowy (i long jest 32-bitowy), std::int32_t będzie aliasem dla long .

A co z 8-bitowymi typami całkowitymi o stałej szerokości?

W większości przypadków std::int8_t jest pseudonimem dla signed char ponieważ jest to jedyny dostępny 8-bitowy typ całkowity ze znakiem (bool i char nie są uważane za typy całkowite ze znakiem). W takim przypadku std::int8_t będzie zachowywał się jak znak na tej platformie.

Jednak w rzadkich przypadkach, jeśli platforma ma specyficzny dla implementacji 8-bitowy typ całkowity ze znakiem, implementacja może zdecydować o utworzeniu std::int8_t aliasu dla tego typu. W takim przypadku std::int8_t będzie zachowywał się jak ten typ, który może bardziej przypominać int niż znak.

std::uint8_t zachowuje się podobnie.

Inne wady stałej szerokości

Liczby całkowite o stałej szerokości mają pewne potencjalne wady:

Po pierwsze, nie ma gwarancji, że liczby całkowite o stałej szerokości zostaną zdefiniowane we wszystkich architekturach. Istnieją tylko w systemach, w których istnieją podstawowe typy całkowe pasujące do ich szerokości i zgodne z określoną reprezentacją binarną. Twój program nie skompiluje się na żadnej architekturze, która nie obsługuje liczby całkowitej o stałej szerokości używanej przez Twój program. Jednakże, biorąc pod uwagę, że nowoczesne architektury standaryzują zmienne 8/16/32/64-bitowe, jest mało prawdopodobne, aby stanowiło to problem, chyba że Twój program musi być przenośny na egzotyczne architektury mainframe lub wbudowane.

Po drugie, jeśli używasz liczby całkowitej o stałej szerokości, może ona być wolniejsza niż szerszy typ w niektórych architekturach. Na przykład, jeśli potrzebujesz liczby całkowitej o gwarantowanej długości 32 bitów, możesz zdecydować się na użycie std::int32_t, ale Twój procesor może w rzeczywistości szybciej przetwarzać 64-bitowe liczby całkowite. Jednak to, że Twój procesor może przetwarzać dany typ szybciej, nie oznacza, że ​​Twój program będzie ogólnie szybszy — współczesne programy są często ograniczone przez użycie pamięci, a nie procesora, a większe zużycie pamięci może spowolnić program bardziej niż szybsze przetwarzanie procesora go przyspiesza. Trudno to stwierdzić bez faktycznego pomiaru.

To jednak tylko drobne sprzeczki.

Typy szybkie i najmniej całkowe Opcjonalne

Aby pomóc zaradzić powyższym wadom, C++ definiuje również dwa alternatywne zestawy liczb całkowitych, które z pewnością istnieją.

Szybkie typy (std::int_fast#_t i std::uint_fast#_t) zapewniają najszybszy typ całkowity ze znakiem/bez znaku o szerokości co najmniej # bitów (gdzie # = 8, 16, 32 lub 64). Na przykład std::int_fast32_t poda najszybszy typ liczby całkowitej ze znakiem, który ma co najmniej 32 bity. Przez najszybszy rozumiemy typ całkowity, który może zostać najszybciej przetworzony przez procesor.

Typy najmniejsze (std::int_least#_t i std::uint_least#_t) zapewniają najmniejszy typ całkowity ze znakiem/bez znaku o szerokości co najmniej # bitów (gdzie # = 8, 16, 32 lub 64). Na przykład std::uint_least32_t poda najmniejszy typ liczby całkowitej bez znaku, który ma co najmniej 32 bity.

Oto przykład z autorskiego Visual Studio (32-bitowa aplikacja konsolowa):

#include <cstdint> // for fast and least types
#include <iostream>

int main()
{
	std::cout << "least 8:  " << sizeof(std::int_least8_t)  * 8 << " bits\n";
	std::cout << "least 16: " << sizeof(std::int_least16_t) * 8 << " bits\n";
	std::cout << "least 32: " << sizeof(std::int_least32_t) * 8 << " bits\n";
	std::cout << '\n';
	std::cout << "fast 8:  "  << sizeof(std::int_fast8_t)   * 8 << " bits\n";
	std::cout << "fast 16: "  << sizeof(std::int_fast16_t)  * 8 << " bits\n";
	std::cout << "fast 32: "  << sizeof(std::int_fast32_t)  * 8 << " bits\n";

	return 0;
}

To dało wynik:

least 8:  8 bits
least 16: 16 bits
least 32: 32 bits

fast 8:  8 bits
fast 16: 32 bits
fast 32: 32 bits

Widzisz, że std::int_least16_t to 16 bitów, podczas gdy std::int_fast16_t w rzeczywistości 32-bitowe. Dzieje się tak, ponieważ na maszynie autora 32-bitowe liczby całkowite są przetwarzane szybciej niż 16-bitowe liczby całkowite.

Jako inny przykład, załóżmy, że mamy architekturę, która ma tylko 16-bitowe i 64-bitowe typy całkowite. std::int32_t nie istniałoby, podczas gdy std::least_int32_t (oraz std::fast_int32_t) wynosiłoby 64 bitów.

Te szybkie i najmniejsze liczby całkowite mają jednak swoje wady. Po pierwsze, niewielu programistów faktycznie z nich korzysta, a brak znajomości może prowadzić do błędów. Wtedy szybkie typy mogą również prowadzić do marnowania pamięci, ponieważ ich rzeczywisty rozmiar może być znacznie większy niż wskazuje ich nazwa.

A co najważniejsze, ponieważ rozmiar szybkich/najmniejszych liczb całkowitych jest zdefiniowany przez implementację, Twój program może wykazywać różne zachowania na architekturach, w których osiągają różne rozmiary. Na przykład:

#include <cstdint>
#include <iostream>

int main()
{
    std::uint_fast16_t sometype { 0 };
    sometype = sometype - 1; // intentionally overflow to invoke wraparound behavior

    std::cout << sometype << '\n';

    return 0;
}

Ten kod da różne wyniki w zależności od tego, czy std::uint_fast16_t ma 16, 32 czy 64 bity! Właśnie tego chcieliśmy uniknąć, używając przede wszystkim liczb całkowitych o stałej szerokości!

Najlepsza praktyka

Unikaj szybkich i najmniej całkowych typów, ponieważ mogą one wykazywać różne zachowania na architekturach, w których rozwiązują się do różnych rozmiarów.

Dobre praktyki dotyczące typów całkowitych

Biorąc pod uwagę różne zalety i wady podstawowych typów całkowitych, typów całkowitych o stałej szerokości, typów całkowitych szybkich/najmniej całkowitych i podpisane/niepodpisane wyzwania, istnieje niewielki konsensus co do integralnych najlepszych praktyk.

Nasze stanowisko jest takie, że lepiej jest mieć rację niż szybko i lepiej ponieść porażkę w czasie kompilacji niż w czasie wykonywania. Dlatego jeśli potrzebujesz typu całkowitego z gwarantowanym zakresem, zalecamy unikanie typów szybkich/najmniejszych na rzecz typów o stałej szerokości. Jeśli później odkryjesz potrzebę obsługi platformy ezoterycznej, dla której nie da się skompilować określonego typu całkowego o stałej szerokości, możesz od tego momentu zdecydować, w jaki sposób przeprowadzić migrację programu (i dokładnie przetestować go ponownie).

Najlepsza praktyka

  • Preferuj int kiedy rozmiar liczby całkowitej nie ma znaczenia (np. liczba zawsze będzie mieścić się w zakresie 2-bajtowej liczby całkowitej ze znakiem). Na przykład, jeśli poprosisz użytkownika o podanie wieku lub odliczenie od 1 do 10, nie ma znaczenia, czy int jest 16-bitowy, czy 32-bitowy (liczby będą pasować w obu przypadkach). Obejmuje to zdecydowaną większość przypadków, z którymi możesz się spotkać.
  • Preferuj std::int#_t podczas przechowywania ilości wymagającej gwarantowanego zakresu.
  • Preferuj std::uint#_t kiedy wymagane jest manipulowanie bitami lub wymagane jest dobrze zdefiniowane zachowanie zawijania (np. w przypadku kryptografii lub generowania liczb losowych).

Unikaj, jeśli to możliwe:

  • short i long liczb całkowitych (preferuj typ całkowity o stałej szerokości zamiast tego).
  • Szybkie i najmniej całkowite typy (preferuj zamiast tego typ całkowity o stałej szerokości).
  • Typy bez znaku do przechowywania ilości (zamiast tego preferuj typ całkowity ze znakiem).
  • 8-bitowe typy całkowite o stałej szerokości (preferuj zamiast tego 16-bitowy typ całkowity o stałej szerokości).
  • Dowolny typ całkowity o stałej szerokości specyficzny dla kompilatora). liczby całkowite (na przykład Visual Studio definiuje __int8, __int16 itp.)

Co to jest std::size_t?

Rozważ następujący kod:

#include <iostream>

int main()
{
    std::cout << sizeof(int) << '\n';

    return 0;
}

Na komputerze autora wypisuje:

4

Całkiem proste, prawda? Możemy wywnioskować, że operator sizeof zwraca wartość całkowitą — ale jakiego typu całkowitego jest ta zwracana wartość? int? Krótki? Odpowiedź jest taka, że sizeof zwraca wartość typu std::size_t. std::size_t jest aliasem dla zdefiniowanego w implementacji typu całkowitego bez znaku. Innymi słowy, kompilator decyduje, czy std::size_t jest unsigned int, unsigned long, unsigned long long itp..

Kluczowa informacja

std::size_t jest aliasem dla zdefiniowanego w implementacji typu całkowitego bez znaku. Jest używany w bibliotece standardowej do reprezentowania rozmiaru bajtów lub długości obiektów.

Dla zaawansowanych czytelników

std::size_t jest w rzeczywistości typedef. W lekcji omówimy typedefs. 10.7 -- Typedefs i aliasy typów.

std::size_t jest zdefiniowany w wielu różnych nagłówkach. Jeśli musisz użyć std::size_t, najlepszym nagłówkiem do uwzględnienia jest <cstddef>, ponieważ zawiera najmniejszą liczbę innych zdefiniowanych identyfikatorów.

Na przykład:

#include <cstddef>  // for std::size_t
#include <iostream>

int main()
{
    int x { 5 };
    std::size_t s { sizeof(x) }; // sizeof returns a value of type std::size_t, so that should be the type of s
    std::cout << s << '\n';

    return 0;
}

Najlepsza praktyka

Jeśli używasz std::size_t jawnie w swoim kodzie, #include jeden z nagłówków definiujących std::size_t (zalecamy <cstddef>).

Użycie sizeof nie wymaga nagłówka (mimo że zwraca wartość, której typ to std::size_t).

Podobnie jak liczba całkowita może różnić się rozmiarem w zależności od systemu, std::size_t również może się różnić rozmiarem. std::size_t jest gwarantowany bez znaku i ma co najmniej 16 bitów, ale w większości systemów będzie równoważny szerokości adresu aplikacji. Oznacza to, że w przypadku aplikacji 32-bitowych std::size_t będzie zazwyczaj 32-bitowa liczba całkowita bez znaku, a w przypadku aplikacji 64-bitowej std::size_t będzie zazwyczaj 64-bitową liczbą całkowitą bez znaku.

Klasa sizeof operator zwraca wartość typu std::size_t Opcjonalne

Nota autora

Poniższe sekcje są lekturą opcjonalną. Zrozumienie poniższych sekcji nie jest konieczne.

Co zabawne, możemy użyć operatora sizeof . (który zwraca wartość typu std::size_t), aby zapytać o rozmiar std::size_t samego:

#include <cstddef> // for std::size_t
#include <iostream>

int main()
{
	std::cout << sizeof(std::size_t) << '\n';

	return 0;
}

Skompilowany jako 32-bitowa (4-bajtowa) aplikacja konsolowa w systemie autora, wypisuje:

4

std::size_t narzuca górny limit rozmiaru obiektu Opcjonalne

Klasa sizeof operator musi być w stanie zwrócić rozmiar obiektu w bajtach jako wartość typu std::size_t. Dlatego rozmiar bajtu obiektu nie może być większy niż największa wartość std::size_t , jaką może pomieścić.

Klasa Standard C++20 ([basic.compound] 1.8.2) mówi: „Konstruowanie typu w taki sposób, że liczba bajtów w jego reprezentacji obiektowej przekracza maksymalną wartość reprezentowaną w typie std::size_t (17.2) jest źle sformułowany.”

Gdyby możliwe było utworzenie większego obiektu, sizeof nie byłby w stanie zwrócić jego rozmiaru w bajtach, ponieważ wykraczałby poza zakres, który mógłby pomieścić a std::size_t . Zatem utworzenie obiektu o rozmiarze (w bajtach) większym niż największa wartość, jaką może pomieścić obiekt typu std::size_t , jest nieprawidłowe (i spowoduje błąd kompilacji).

Załóżmy na przykład, że std::size_t ma w naszym systemie rozmiar 4 bajtów. 4-bajtowy typ całkowity bez znaku ma zakres od 0 do 4 294 967 295. Zatem obiekt 4-bajtowy std::size_t może przechowywać dowolną wartość od 0 do 4 294 967 295. Rozmiar dowolnego obiektu o rozmiarze od 0 do 4 294 967 295 może zostać zwrócony w postaci wartości typu std::size_t, więc nie ma w tym nic złego, jeśli jednak rozmiar obiektu w bajtach jest większy niż 4 294 967 295 bajtów, wówczas sizeof nie będzie w stanie zwrócić rozmiaru. tego obiektu, ponieważ wartość byłaby poza zakresem std::size_t. Dlatego w tym systemie nie można utworzyć żadnego obiektu większego niż 4 294 967 295 bajtów.

Na marginesie…

Rozmiar std::size_t narzuca ścisłe matematyczne górne ograniczenie rozmiaru obiektu. W praktyce największy możliwy do stworzenia obiekt może być mniejszy od tej wielkości (być może znacznie).

Niektóre kompilatory ograniczają największy możliwy do stworzenia obiekt. sprzeciwić się połowie maksymalnej wartości std::size_t (wyjaśnienie tego można znaleźć tutaj).

Inne czynniki mogą również odgrywać rolę, np. ilość ciągłej pamięci, jaką komputer ma do przydzielenia.

Kiedy normą były aplikacje 8- i 16-bitowe, ograniczenie to narzucało znaczne ograniczenie rozmiaru obiektów. W erze systemów 32-bitowych i 64-bitowych jest to rzadko spotykany problem i dlatego na ogół nie trzeba się tym martwić.

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
567 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze