4.6 – Целочисленные типы фиксированной ширины и size_t
В предыдущих уроках, посвященных целочисленным типам, мы говорили о том, что C++ гарантирует только то, что целочисленные переменные будут иметь минимальный размер, но он может быть и больше, в зависимости от целевой системы.
Почему размер целочисленных переменных не фиксирован?
Короткий ответ: это восходит к C, когда компьютеры были медленными, а производительность вызывала наибольшую озабоченность. C решил намеренно оставить размер целого числа открытым, чтобы разработчики компиляторов могли выбрать размер для int
, который лучше всего работает на архитектуре целевого компьютера.
Разве это не отстой?
По современным меркам да. Программисту немного нелепо иметь дело с типами с неопределенными диапазонами значений. Программа, которая использует значения, которые превышают минимальные гарантированные диапазоны значений, может работать на одной архитектуре и не работать на другой.
Целочисленные типы фиксированной ширины
Чтобы облегчить кроссплатформенную переносимость, C99 определил набор целочисленных типов фиксированной ширины (в заголовочном файле stdint.h), которые гарантированно будут иметь одинаковый размер в любой архитектуре.
Они определены следующим образом:
Название | Тип | Диапазон значений | Примечание |
---|---|---|---|
std::int8_t | 1 байт со знаком | от -128 до 127 | Во многих системах обрабатывается как signed char . Смотрите примечание ниже. |
std::uint8_t | 1 байт без знака | от 0 до 255 | Во многих системах обрабатывается как unsigned char . Смотрите примечание ниже. |
std::int16_t | 2 байта со знаком | от -32 768 до 32 767 | |
std::uint16_t | 2 байта без знака | от 0 до 65 535 | |
std::int32_t | 4 байта со знаком | - от -2 147 483 648 до 2 147 483 647 | |
std::uint32_t | 4 байта без знака | от 0 до 4 294 967 295 | |
std::int64_t | 8 байт со знаком | от -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807 | |
std::uint64_t | 8 байт без знака | от 0 до 18 446 744 073 709 551 615 |
C++ официально принял эти целочисленные типы фиксированной ширины как часть стандарта C++11. К ним можно получить доступ, включив заголовочный файл cstdint, где они определены внутри пространства имен std
. Например:
#include <cstdint>
#include <iostream>
int main()
{
std::int16_t i{5};
std::cout << i;
return 0;
}
Целочисленные типы фиксированной ширины имеют два недостатка: во-первых, они являются необязательными и существуют только в том случае, если есть базовые типы, соответствующие их ширине и следующие определенному двоичному представлению. Использование целочисленного типа фиксированной ширины делает ваш код менее портируемым, он может не компилироваться в других системах.
Во-вторых, если вы используете целочисленный тип фиксированной ширины, на некоторых архитектурах он может быть медленнее, чем более широкий тип. Если вам нужен целочисленный тип для хранения значений от -10 до 20, у вас может возникнуть соблазн использовать std::int8_t
. Но ваш процессор мог бы лучше обрабатывать 32-битные целые числа, поэтому вы просто потеряли скорость, сделав ограничение, в котором не было необходимости.
Предупреждение
Приведенных выше целочисленных типов фиксированной ширины следует избегать, поскольку они могут не быть определены на всех целевых архитектурах.
Быстрые и наименьшие по размеру целочисленные типы
Чтобы помочь устранить указанные выше недостатки, C++ также определяет два альтернативных набора целочисленных типов.
Быстрый тип (std::int_fast#_t
) обеспечивает самый быстрый целочисленный тип со знаком с шириной не менее # бит (где # = 8, 16, 32 или 64). Например, std::int_fast32_t
предоставит вам самый быстрый целочисленный тип со знаком, имеющий как минимум 32 бита.
Наименьший по размеру тип (std::int_least#_t
) предоставляет наименьший по размеру целочисленный тип со знаком с шириной не менее # бит (где # = 8, 16, 32 или 64). Например, std::int_least32_t
предоставит вам наименьший целочисленный тип со знаком, имеющий как минимум 32 бита.
Вот пример программы, скомпилированной автором в Visual Studio (32-разрядное консольное приложение):
#include <cstdint>
#include <iostream>
int main()
{
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";
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";
return 0;
}
В результате эта программа дает следующий вывод:
fast 8: 8 bits
fast 16: 32 bits
fast 32: 32 bits
least 8: 8 bits
least 16: 16 bits
least 32: 32 bits
Вы можете видеть, что std::int_fast16_t
был 32-битным, тогда как std::int_least16_t
был 16-битным.
Существует также набор быстрых и минимальных по размеру типов без знака (std::uint_fast#_t
и std::uint_least#_t
).
Эти быстрые и минимальные по размеру типы гарантированно определены и безопасны в использовании.
Лучшая практика
Выбирайте целочисленные типы std::int_fast#_t
и std::int_least#_t
, когда вам нужно, чтобы целое число гарантированно было не менее определенного минимального размера.
Предупреждение: std::int8_t
и std::uint8_t
могут вести себя как символы вместо целых чисел
Примечание: о символах мы подробнее поговорим в уроке «4.11 – Символы».
Из-за упущения в спецификации C++ большинство компиляторов определяют и обрабатывают std::int8_t
и std::uint8_t
(и соответствующие быстрые и наименьшие по размеру фиксированные типы) идентично типам signed char
и unsigned char
соответственно. Следовательно, std::cin
и std::cout
могут работать иначе, чем вы ожидаете. Ниже приведен пример программы, показывающей это:
#include <cstdint>
#include <iostream>
int main()
{
std::int8_t myint{65};
std::cout << myint;
return 0;
}
В большинстве систем эта программа будет печатать 'A' (обрабатывая myint
как символ). Однако в некоторых системах может быть напечатано 65, как и ожидалось.
Для простоты лучше вообще избегать std::int8_t
и std::uint8_t
(и связанных с ними быстрых и наименьших типов) (вместо этого используйте std::int16_t
или std::uint16_t
). Однако если вы всё же используете std::int8_t
или std::uint8_t
, вам следует быть осторожными со всем, что могло бы интерпретировать std::int8_t
или std::uint8_t
как символ вместо целого числа (включая std::cout
и std::cin
).
Надеюсь, это будет разъяснено в будущем черновике стандарта C++.
Предупреждение
Избегайте 8-битных целочисленных типов фиксированной ширины. Если вы их используете, обратите внимание, что они часто обрабатываются как символы.
Лучшие практики работы с целочисленными типами
Теперь, когда в C++ добавлены целочисленные типы фиксированной ширины, передовой практикой для целочисленных значений в C++ является следующее:
- Следует предпочесть использование
int
, когда размер целого числа не имеет значения (например, число всегда будет соответствовать диапазону значений 2-байтового целочисленного типа со знаком). Например, если вы просите пользователя ввести свой возраст или считаете от 1 до 10, не имеет значения, равен лиint
16 или 32 битам (числа подходят в любом случае). Это покроет подавляющее большинство случаев, с которыми вы, вероятно, столкнетесь. - Если вам нужна переменная гарантированно определенного размера, и вы хотите повысить производительность, используйте
std::int_fast#_t
. - Если вам нужна переменная, гарантированно имеющая определенный размер, и вы хотите отдать предпочтение экономии памяти над производительностью, используйте
std::int_least#_t
. Он чаще всего используется при распределении большого количества переменных.
По возможности избегайте следующего:
- Беззнаковые типы, если у вас нет веской причины.
- 8-битные целочисленные типы фиксированной ширины.
- Любые специфичные для компилятора целочисленные типы фиксированной ширины – например, Visual Studio определяет
__int8
,__int16
и т.д.
Что такое std::size_t
?
Рассмотрим следующий код:
#include <iostream>
int main()
{
std::cout << sizeof(int) << '\n';
return 0;
}
На машине автора эта программа печатает:
4
Довольно просто, правда? Мы можем сделать вывод, что оператор sizeof
возвращает целочисленное значение, но какой целочисленный тип у этого значения? int
? short
? Ответ заключается в том, что sizeof
(и многие функции, возвращающие значение размера или длины) возвращают значение типа std::size_t
. std::size_t
определяется как целочисленный тип без знака и обычно используется для представления размера или длины объектов.
Забавно, но мы можем использовать оператор sizeof
(который возвращает значение типа std::size_t
), чтобы запросить размер самого std::size_t
:
#include <cstddef> // std::size_t
#include <iostream>
int main()
{
std::cout << sizeof(std::size_t) << '\n';
return 0;
}
Скомпилированная как 32-битное (4 байтовое) консольное приложение в системе автора данная программа выводит:
4
Подобно целочисленному типу, размер которого зависит от системы, размер std::size_t
также может быть разным. std::size_t
гарантированно является беззнаковым и имеет не менее 16 бит, но в большинстве систем будет эквивалентен ширине адреса в приложении. То есть для 32-разрядных приложений std::size_t
обычно будет 32-разрядным целочисленным типом без знака, а для 64-разрядного приложения size_t
обычно будет 64-разрядным целочисленным типом без знака. size_t
определяется достаточно большим, чтобы вместить размер самого большого объекта, созданного в вашей системе (в байтах). Например, если std::size_t
имеет ширину 4 байта, самый большой объект, создаваемый в вашей системе, не может быть больше 4 294 967 295 байтов, потому что это наибольшее число, которое может хранить 4-байтовое целое число без знака. Это только верхний предел размера объекта, реальный предел размера может быть ниже в зависимости от используемого компилятора.
По определению, любой объект, размер которого превышает максимальное значение, которое может содержать size_t
, считается неправильно сформированным (и вызовет ошибку компиляции), поскольку оператор sizeof
не сможет вернуть размер без выполнения циклического переноса.